# wireframe.md — W0 hierarchy, ASCII, component sizes and pseudocode

Each parent defines the shared bones and effects. Children supply view-model data and small declared variations, not duplicate layout/lifecycle code. The exact order is: parent description → Wide ASCII → Compact ASCII → Vertical/Mobile ASCII → child a with all three views → child b, and so on. Drawings are schematic, not fixed pixel sizes.

**W0: Master shell** — title top-left, exit top-right, Back bottom-left, Primary bottom-right; shared body host and effects.

**W1: Workspace/modal** — Settings, Town, Creation, service selection, inspection, save flows, and choice bodies.

**W2: Confirmation** — short decisions with shared focus, cancellation, and confirmation behavior.

**W3: Main menu** — one screen-centered title and contextual menu/preview state.

**W4: Gameplay/encounter** — shared HUD, scene, context region, and bottom controls; combat, map, and dialogue use registered body variants.

**Inherited placement:** title top-left, exit top-right, Close/Back bottom-left, interaction/confirmation bottom-right, all with shared consistent inset/padding. A single footer button spans the full usable footer width; its label is centered. Two footer buttons keep their left/right roles. No footer buttons means no empty footer. W1–W4 and all children inherit this; omitted components do not shift remaining anchors. Explicit owner variants remain W3’s screen-centered title/Profile header slot and W4a’s tightly centered combat group. Capabilities omit inapplicable actions without inventing navigation or duplicating a primary command.

**Footer is always inline:** one horizontal row in Wide, Compact, and Vertical/Mobile. Never stack footer buttons or wrap their labels. Use concise meaningful labels and shared responsive sizing while preserving minimum input targets. One button fills the row; multiple controls keep their declared positions.

**Inherited palette/state effects:** see COLOR-INTERACTION-CONTRACT.md. Five configurable groups drive surfaces, text, gold accents, positive green, and exit/danger red. Close/Back/Exit highlights red; ordinary ready/selected primary actions highlight green; neutral/unready controls use dark brown/gold. Disabled/busy wins. Destructive confirmations and the earlier End Turn guidance rule are named exceptions, as are semantic resource/tag/rarity/target colors and media. These are inherited state mappings, not child-specific hex colors. Keep related content single-row where readable; state effects never move anchors.

Parent effects include spacing, elevation, focus/hover, opening/closing transitions, reduced motion, and input ownership where applicable. Domain effects remain explicit registered commands. Orientation is a presentation variant, never another child. W1 is the default modal; W2 shares its primitives for compact decisions. These document IDs do not rename existing saved/semantic layout IDs. A = actions, D = draw pile, E = discard/exhaust, P = potions; real controls have full accessible labels and valid targets.

**Component dimensions:** every view has a companion table naming each structural slot and its nominal viewport-relative allocation. See COMPONENT-SIZING.md for units, parent-relative conversion, optional slots, padding, and minimum-size overrides. These are proposed layout targets, not measured game geometry.

**Current specification:** [CURRENT-SPECIFICATION.md](CURRENT-SPECIFICATION.md) is authoritative over earlier nominal/iteration notes.

## Wireframe W0: Master shell

**Base wireframe.** Placement anchors: TITLE top-left; EXIT (×) top-right; CLOSE/BACK bottom-left; INTERACTION/CONFIRMATION bottom-right. All anchors use the same shared inset/padding, never label-dependent offsets. Header/body/footer have reserved layout space; content cannot displace or cover the controls. With exactly ONE footer button, that button spans the full usable footer width with a centered label. With two, retain left/right roles and consistent sizing/padding. With zero, omit the footer. Unneeded components may be omitted by explicit capability, without shifting remaining anchors. All four families and their children inherit this positioning, effects and lifecycle.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ {Title}                                    [×] │
├────────────────────────────────────────────────┤
│                                                │
│ {Body supplied by W1–W4}                       │
│                                                │
│                                                │
├────────────────────────────────────────────────┤
│ [Close / Back]                       [Primary] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W0.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W0.header` | 95vw | 10vh | W0.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W0.header.title` | 77vw | 6vh | W0.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W0.header.exit` | 8vw | 6vh | W0.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W0.body` | 95vw | 70vh | W0.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W0.body.activePane` | 90vw | 66vh | W0.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W0.body.content` | 90vw | 66vh | W0.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W0.footer` | 95vw | 10vh | W0.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W0.footer.closeBack` | 44vw | 6vh | W0.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W0.footer.primary` | 44vw | 6vh | W0.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W0.footer.singleAction` | 90vw | 6vh | W0.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ {Title}                      [×] │
├──────────────────────────────────┤
│ {Body from parent variant}       │
│                                  │
├──────────────────────────────────┤
│ [Close / Back]         [Primary] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W0.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W0.header` | 95vw | 10vh | W0.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W0.header.title` | 77vw | 6vh | W0.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W0.header.exit` | 8vw | 6vh | W0.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W0.body` | 95vw | 70vh | W0.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W0.body.activePane` | 90vw | 66vh | W0.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W0.body.content` | 90vw | 66vh | W0.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W0.footer` | 95vw | 10vh | W0.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W0.footer.closeBack` | 44vw | 6vh | W0.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W0.footer.primary` | 44vw | 6vh | W0.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W0.footer.singleAction` | 90vw | 6vh | W0.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ {Title}               [×] │
├───────────────────────────┤
│                           │
│ {Body from parent}        │
│                           │
│                           │
│                           │
├───────────────────────────┤
│ [Close / Back]  [Primary] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W0.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W0.header` | 95vw | 10vh | W0.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W0.header.title` | 77vw | 6vh | W0.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W0.header.exit` | 8vw | 6vh | W0.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W0.body` | 95vw | 70vh | W0.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W0.body.activePane` | 90vw | 66vh | W0.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W0.body.content` | 90vw | 66vh | W0.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W0.footer` | 95vw | 10vh | W0.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W0.footer.closeBack` | 44vw | 6vh | W0.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W0.footer.primary` | 44vw | 6vh | W0.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W0.footer.singleAction` | 90vw | 6vh | W0.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ {Title}               [×] │
├───────────────────────────┤
│                           │
│ {Body from parent}        │
│                           │
│                           │
│                           │
├───────────────────────────┤
│ [Close / Back]  [Primary] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W0.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W0.header` | 95vw | 10vh | W0.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W0.header.title` | 77vw | 6vh | W0.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W0.header.exit` | 8vw | 6vh | W0.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W0.body` | 95vw | 70vh | W0.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W0.body.activePane` | 90vw | 66vh | W0.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W0.body.content` | 90vw | 66vh | W0.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W0.footer` | 95vw | 10vh | W0.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W0.footer.closeBack` | 44vw | 6vh | W0.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W0.footer.primary` | 44vw | 6vh | W0.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W0.footer.singleAction` | 90vw | 6vh | W0.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: definitionId, immutableViewModel, commandBindings, host, inputContext
OUTPUT: viewHandle(update, dispose), semantic intents

FUNCTION MountMaster(inputs):
    definition = ResolveValidatedInheritance(definitionId)
    REQUIRE definition ancestry ends at W0; reject cycles/unknown fields
    viewport = SharedViewportAdapter.AvailableLocalSize(host)
    mode = ChooseContentFitMode(viewport, textScale, targetMinimums)
    shell = CreateHeaderBodyFooterOnce()
    ReserveHeaderAndFooterOutsideBodyScroll(shell)
    ApplyInheritedPaddingSpacingEffects(shell, definition)
    Anchor(title, TOP_LEFT); Anchor(exit, TOP_RIGHT)
    IF definition.titlePlacement == screenCenter: CenterTitleOnFullHost()
    RenderOnlyApplicableComponents(immutableViewModel.capabilities)
    RenderRegisteredBody(immutableViewModel.body)
    RenderFooter(immutableViewModel.actions, definition.footerPlacement)
    BindTopmostInputFocusAndDismissal(shell, definition.interactionPolicy)
    RETURN handle that updates stable nodes and disposes listeners/audio/timers

FUNCTION RenderFooter(actions, placement):
    visible = FilterByDeclaredVisibility(actions) // disabled visible actions count
    IF count(visible) == config.referenceTokens.value_0.value: OmitFooter(); RETURN
    IF count(visible) == config.referenceTokens.value_1.value:
        PlaceOneButtonFullUsableWidth(labelCentered = true); RETURN
    IF placement == packedCombat:
        CenterOneGroup(Actions, Draw, EndTurn, DiscardExhaust, Potions)
        UseMinimalSharedGapsAndPairedSideSizes()
    ELSE:
        Anchor(CloseOrBack, BOTTOM_LEFT); Anchor(Primary, BOTTOM_RIGHT)
        IF declaredMiddleAction exists: Anchor(declaredMiddleAction, CENTER)
    KeepButtonsAndLabelsOnOneHorizontalRow()
    UseConciseLabelsAndSharedSizingWithinTextAndTargetMinimums()
    IF supported size still cannot fit: ReportLayoutFailure(); never silently stack

FUNCTION ResolveControlStyle(action, inputState):
    IF action absent by capability: RETURN absent
    IF action.busy OR NOT action.canActivate: RETURN neutralBrownGoldMuted
    IF action has registeredException: RETURN ResolveException(action, inputState)
    highlighted = hover OR keyboardFocus OR controllerFocus OR selected
    IF action.role == exit: RETURN highlighted ? redEmphasis : brownGold
    IF action.role == primary: RETURN highlighted ? strongGreen : readyGreen
    RETURN highlighted ? goldEmphasis : brownGold

ON resize/orientation/textScale:
    ReprojectLayoutOnly(); PreserveSelectionFocusAndInProgressDomainState()
ON Close/Back/Primary:
    EmitBoundIntentIfAllowed(); never infer behavior from label or color
ON dispose:
    CloseOnceWithReason(dispose); ReleaseAllOwnedResources()
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/modalShell.js:46](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L46) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Body": [
      {
        "label": "Model",
        "value": "Immutable snapshot",
        "kind": "fact"
      },
      {
        "label": "Input",
        "value": "Semantic intent",
        "kind": "fact"
      },
      {
        "label": "Layout",
        "value": "Header / active body / inline footer",
        "kind": "fact"
      }
    ]
  },
  "primary": "Confirm",
  "references": [
    {
      "file": "src/ui/components/modalShell.js",
      "symbol": "modalCloseButton",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe W1: Base workspace / modal

**Parent: W0.** Default base modal: one header, category rail, active pane, footer. Compact/mobile moves category navigation above content. Single-category variants may omit the rail. selection body/inspection body/choice body bodies compose inside this frame; do not nest whole modal shells. Selection/detail, inspection/status, and choice/progression are body variants of this same frame. A child without categories omits the rail. Its body differences do not create a new parent renderer.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ {Title}                                    [×] │
├──────────────┬─────────────────────────────────┤
│ Category A   │ {Active body model}             │
│ Category B   │ {Control / choice}              │
│ Category C   │ {Useful feedback}               │
├──────────────┴─────────────────────────────────┤
│ [Back / secondary]                   [Primary] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1.header` | 95vw | 10vh | W1.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1.header.title` | 77vw | 6vh | W1.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1.header.exit` | 8vw | 6vh | W1.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1.body` | 95vw | 70vh | W1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1.body.navigation` | 21.6vw | 66vh | W1.body | top-left | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Left category rail |
| `W1.body.activePane` | 65.9vw | 66vh | W1.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1.body.activePane` | 65.9vw | 66vh | W1.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1.footer` | 95vw | 10vh | W1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1.footer.closeBack` | 44vw | 6vh | W1.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1.footer.primary` | 44vw | 6vh | W1.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1.footer.singleAction` | 90vw | 6vh | W1.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ {Title}                      [×] │
│ [Category ▾]                     │
├──────────────────────────────────┤
│ {Active body model}              │
│ {Control / choice}               │
│ {Useful feedback}                │
├──────────────────────────────────┤
│ [Back / secondary]     [Primary] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1.header` | 95vw | 10vh | W1.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1.header.title` | 77vw | 6vh | W1.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1.header.exit` | 8vw | 6vh | W1.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1.body` | 95vw | 70vh | W1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1.body.navigation` | 90vw | 6vh | W1.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1.body.activePane` | 90vw | 58vh | W1.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1.body.activePane` | 90vw | 58vh | W1.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1.footer` | 95vw | 10vh | W1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1.footer.closeBack` | 44vw | 6vh | W1.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1.footer.primary` | 44vw | 6vh | W1.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1.footer.singleAction` | 90vw | 6vh | W1.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ {Title}               [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ {Active body model}       │
│ {Control / choice}        │
│ {Useful feedback}         │
├───────────────────────────┤
│ [Back]          [Primary] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1.header` | 95vw | 10vh | W1.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1.header.title` | 77vw | 6vh | W1.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1.header.exit` | 8vw | 6vh | W1.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1.body` | 95vw | 70vh | W1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1.body.navigation` | 90vw | 6vh | W1.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1.body.activePane` | 90vw | 58vh | W1.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1.body.activePane` | 90vw | 58vh | W1.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1.footer` | 95vw | 10vh | W1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1.footer.closeBack` | 44vw | 6vh | W1.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1.footer.primary` | 44vw | 6vh | W1.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1.footer.singleAction` | 90vw | 6vh | W1.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ {Title}               [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ {Active body model}       │
│ {Control / choice}        │
│ {Useful feedback}         │
├───────────────────────────┤
│ [Back]          [Primary] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1.header` | 95vw | 10vh | W1.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1.header.title` | 77vw | 6vh | W1.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1.header.exit` | 8vw | 6vh | W1.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1.body` | 95vw | 70vh | W1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1.body.navigation` | 90vw | 6vh | W1.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1.body.activePane` | 90vw | 58vh | W1.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1.body.activePane` | 90vw | 58vh | W1.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1.footer` | 95vw | 10vh | W1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1.footer.closeBack` | 44vw | 6vh | W1.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1.footer.primary` | 44vw | 6vh | W1.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1.footer.singleAction` | 90vw | 6vh | W1.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: workspaceDefinition, categories, activeCategoryId, bodyModel, actions
INHERITS: W0 geometry, footer, palette, transitions, focus, cleanup

FUNCTION RenderWorkspace(model):
    validCategory = ResolveAvailableCategory(model.activeCategoryId)
    navigation = IF count(categories) > config.referenceTokens.value_1.value THEN
        wide: LeftRail(categories)
        compact/portrait: AccessibleCategorySelector(categories)
      ELSE absent
    body = ProjectOnlyActiveCategory(validCategory)
    RETURN W0.MountOrUpdate(header, navigation, body, actions)

ON chooseCategory(id):
    REQUIRE id is available
    SaveLocalSelectionForCurrentCategory()
    DisposeCurrentBodyBindings(); set activeCategoryId = id
    RestoreLocalSelectionFor(id); update same shell
    DoNotCommitGameplayMerelyBecauseCategoryChanged()

Selection, inspection and choices are registered BODY variants here.
Do not mount a second W0 inside the body.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/modalShell.js:75](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L75) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Overview": [
      {
        "label": "Selected category",
        "value": "Overview",
        "kind": "fact"
      },
      {
        "label": "Shared body",
        "value": "Registered component composition",
        "kind": "fact"
      }
    ],
    "Details": [
      {
        "label": "State",
        "value": "Changes with selected category",
        "kind": "fact"
      }
    ]
  },
  "primary": null,
  "references": [
    {
      "file": "src/ui/components/modalShell.js",
      "symbol": "modalFooter",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1a: Settings

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Settings                                   [×] │
├──────────────┬─────────────────────────────────┤
│ Display ●    │ Text size          [control]    │
│ Audio        │ UI scale           [control]    │
│ Accessibility│ Necessary help, if needed       │
│ Advanced     │                                 │
├──────────────┴─────────────────────────────────┤
│ [             Applicable action              ] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1a.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1a.header` | 95vw | 10vh | W1a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1a.header.title` | 77vw | 6vh | W1a.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1a.header.exit` | 8vw | 6vh | W1a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1a.body` | 95vw | 70vh | W1a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1a.body.navigation` | 21.6vw | 66vh | W1a.body | top-left | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Left category rail |
| `W1a.body.activePane` | 65.9vw | 66vh | W1a.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1a.body.fields` | 31.95vw | 66vh | W1a.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1a.body.feedback` | 31.95vw | 66vh | W1a.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1a.footer` | 95vw | 10vh | W1a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1a.footer.closeBack` | 44vw | 6vh | W1a.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1a.footer.primary` | 44vw | 6vh | W1a.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1a.footer.singleAction` | 90vw | 6vh | W1a.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Settings                     [×] │
│ [Category ▾]                     │
├──────────────────────────────────┤
│ Text size          [control]     │
│ UI scale           [control]     │
│ Necessary help, if needed        │
├──────────────────────────────────┤
│ [      Applicable action       ] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1a.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1a.header` | 95vw | 10vh | W1a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1a.header.title` | 77vw | 6vh | W1a.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1a.header.exit` | 8vw | 6vh | W1a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1a.body` | 95vw | 70vh | W1a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1a.body.navigation` | 90vw | 6vh | W1a.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1a.body.activePane` | 90vw | 58vh | W1a.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1a.body.fields` | 90vw | 28vh | W1a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1a.body.feedback` | 90vw | 28vh | W1a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1a.footer` | 95vw | 10vh | W1a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1a.footer.closeBack` | 44vw | 6vh | W1a.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1a.footer.primary` | 44vw | 6vh | W1a.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1a.footer.singleAction` | 90vw | 6vh | W1a.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Settings              [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ Text size                 │
│ [control]                 │
│ UI scale                  │
│ [control]                 │
│ Necessary help, if needed │
├───────────────────────────┤
│ [   Applicable action   ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1a.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1a.header` | 95vw | 10vh | W1a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1a.header.title` | 77vw | 6vh | W1a.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1a.header.exit` | 8vw | 6vh | W1a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1a.body` | 95vw | 70vh | W1a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1a.body.navigation` | 90vw | 6vh | W1a.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1a.body.activePane` | 90vw | 58vh | W1a.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1a.body.fields` | 90vw | 28vh | W1a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1a.body.feedback` | 90vw | 28vh | W1a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1a.footer` | 95vw | 10vh | W1a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1a.footer.closeBack` | 44vw | 6vh | W1a.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1a.footer.primary` | 44vw | 6vh | W1a.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1a.footer.singleAction` | 90vw | 6vh | W1a.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Settings              [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ Text size                 │
│ [control]                 │
│ UI scale                  │
│ [control]                 │
│ Necessary help, if needed │
├───────────────────────────┤
│ [   Applicable action   ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1a.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1a.header` | 95vw | 10vh | W1a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1a.header.title` | 77vw | 6vh | W1a.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1a.header.exit` | 8vw | 6vh | W1a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1a.body` | 95vw | 70vh | W1a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1a.body.navigation` | 90vw | 6vh | W1a.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1a.body.activePane` | 90vw | 58vh | W1a.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1a.body.fields` | 90vw | 28vh | W1a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1a.body.feedback` | 90vw | 28vh | W1a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1a.footer` | 95vw | 10vh | W1a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1a.footer.closeBack` | 44vw | 6vh | W1a.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1a.footer.primary` | 44vw | 6vh | W1a.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1a.footer.singleAction` | 90vw | 6vh | W1a.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: settingsSnapshot, settingsDefinitions, browserCapabilities
PARENT: W1
model.categories = ProjectRegisteredSettingCategories()
model.body = ProjectSettingRows(activeCategory, values, capabilities)
model.actions = ProjectExistingSettingsActions()
RenderWithW1(model)
ON changeSetting(id, value):
    ValidateTypeRangeAndCapability(id, value)
    DispatchSettingsCommandUsingItsExistingApplyOrImmediateSavePolicy()
    RefreshValuesAndFeedback(); do not navigate or rebuild the shell
ON failedBrowserCapability: ShowReasonWithoutPretendingChangeSucceeded()
Palette controls edit shared theme records, not per-component colors.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/settings.js:625](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/settings.js#L625) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Display": [
      {
        "label": "Text size",
        "value": "100",
        "kind": "range"
      },
      {
        "label": "UI scale",
        "value": "100",
        "kind": "range"
      }
    ],
    "Audio": [
      {
        "label": "Music",
        "value": "true",
        "kind": "toggle"
      },
      {
        "label": "Volume",
        "value": "65",
        "kind": "range"
      }
    ],
    "Accessibility": [
      {
        "label": "Reduced motion",
        "value": "false",
        "kind": "toggle"
      },
      {
        "label": "High contrast",
        "value": "false",
        "kind": "toggle"
      }
    ],
    "Advanced": [
      {
        "label": "Positioning guides",
        "value": "true",
        "kind": "toggle"
      }
    ],
    "Changelog": [
      {
        "label": "Changes",
        "value": "Read release records",
        "kind": "fact"
      }
    ],
    "About": [
      {
        "label": "Game",
        "value": "AshenSpire",
        "kind": "fact"
      }
    ]
  },
  "primary": null,
  "references": [
    {
      "file": "src/ui/screens/settings.js",
      "symbol": "settingsCategories",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1b: Town

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. No separate scene/HUD band above the workspace.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Town · Resources                           [×] │
├──────────────┬─────────────────────────────────┤
│ Smith ●      │ [Small NPC portrait] NPC        │
│ Merchant     │ Active service choices          │
│ Rest         │ [Option] [Option]               │
│ Quest NPC    │ Cost / availability             │
├──────────────┴─────────────────────────────────┤
│ [Leave]                          [Talk / Open] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1b.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1b.header` | 95vw | 10vh | W1b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1b.header.title` | 77vw | 6vh | W1b.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1b.header.exit` | 8vw | 6vh | W1b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1b.body` | 95vw | 70vh | W1b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1b.body.navigation` | 21.6vw | 66vh | W1b.body | top-left | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Left category rail |
| `W1b.body.activePane` | 65.9vw | 66vh | W1b.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1b.body.npcIdentity` | 65.9vw | 20.67vh | W1b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1b.body.serviceChoices` | 65.9vw | 20.67vh | W1b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1b.body.feedback` | 65.9vw | 20.67vh | W1b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1b.footer` | 95vw | 10vh | W1b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1b.footer.closeBack` | 44vw | 6vh | W1b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1b.footer.primary` | 44vw | 6vh | W1b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1b.footer.singleAction` | 90vw | 6vh | W1b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Town · Resources             [×] │
│ [Category ▾]                     │
├──────────────────────────────────┤
│ [Small NPC portrait] NPC         │
│ Active service choices           │
│ [Option] [Option]                │
│ Cost / availability              │
├──────────────────────────────────┤
│ [Leave]            [Talk / Open] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1b.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1b.header` | 95vw | 10vh | W1b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1b.header.title` | 77vw | 6vh | W1b.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1b.header.exit` | 8vw | 6vh | W1b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1b.body` | 95vw | 70vh | W1b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1b.body.navigation` | 90vw | 6vh | W1b.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1b.body.activePane` | 90vw | 58vh | W1b.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1b.body.npcIdentity` | 90vw | 18vh | W1b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1b.body.serviceChoices` | 90vw | 18vh | W1b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1b.body.feedback` | 90vw | 18vh | W1b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1b.footer` | 95vw | 10vh | W1b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1b.footer.closeBack` | 44vw | 6vh | W1b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1b.footer.primary` | 44vw | 6vh | W1b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1b.footer.singleAction` | 90vw | 6vh | W1b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Town · Resources      [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ [Small NPC portrait] NPC  │
│ Active service choices    │
│ [Option] [Option]         │
│ Cost / availability       │
├───────────────────────────┤
│ [Leave]     [Talk / Open] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1b.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1b.header` | 95vw | 10vh | W1b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1b.header.title` | 77vw | 6vh | W1b.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1b.header.exit` | 8vw | 6vh | W1b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1b.body` | 95vw | 70vh | W1b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1b.body.navigation` | 90vw | 6vh | W1b.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1b.body.activePane` | 90vw | 58vh | W1b.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1b.body.npcIdentity` | 90vw | 18vh | W1b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1b.body.serviceChoices` | 90vw | 18vh | W1b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1b.body.feedback` | 90vw | 18vh | W1b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1b.footer` | 95vw | 10vh | W1b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1b.footer.closeBack` | 44vw | 6vh | W1b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1b.footer.primary` | 44vw | 6vh | W1b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1b.footer.singleAction` | 90vw | 6vh | W1b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Town · Resources      [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ [Small NPC portrait] NPC  │
│ Active service choices    │
│ [Option] [Option]         │
│ Cost / availability       │
├───────────────────────────┤
│ [Leave]     [Talk / Open] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1b.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1b.header` | 95vw | 10vh | W1b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1b.header.title` | 77vw | 6vh | W1b.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1b.header.exit` | 8vw | 6vh | W1b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1b.body` | 95vw | 70vh | W1b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1b.body.navigation` | 90vw | 6vh | W1b.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1b.body.activePane` | 90vw | 58vh | W1b.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1b.body.npcIdentity` | 90vw | 18vh | W1b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1b.body.serviceChoices` | 90vw | 18vh | W1b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1b.body.feedback` | 90vw | 18vh | W1b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1b.footer` | 95vw | 10vh | W1b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1b.footer.closeBack` | 44vw | 6vh | W1b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1b.footer.primary` | 44vw | 6vh | W1b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1b.footer.singleAction` | 90vw | 6vh | W1b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: townDefinition, playerSnapshot, npcAndServiceRegistries
PARENT: W1
available = EvaluateTownMembershipAndProgressionPredicates()
model.categories = ProjectNPCServiceCategories(available)
model.header = TownNameAndConciseResources()
model.body = SmallNPCPortraitAndActiveServiceModel(selectedNPC)
model.actions = LeaveAndTalkOrOpenFromDomainCapabilities()
RenderWithW1(model) // no extra scene or HUD band
ON chooseNPC(id): ChangeLocalCategorySelectionOnly(id)
ON Talk/Open: NavigateToRegisteredDialogueOrService(selectedNPC)
ON return: RestoreSelectedNPCAndRefreshAvailability()
ON Leave/Close: DispatchApprovedTownExitIntent(); never bypass a required choice
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/rest.js:98](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/rest.js#L98) — Current owner checkout (including inspected local edits)
- [src/ui/screens/smithServices.js:26](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/smithServices.js#L26) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Smith": [
      {
        "label": "Service",
        "value": "Upgrade equipment",
        "kind": "fact"
      },
      {
        "label": "Availability",
        "value": "Choose equipment to view cost",
        "kind": "fact"
      }
    ],
    "Merchant": [
      {
        "label": "Service",
        "value": "Browse offers",
        "kind": "fact"
      }
    ],
    "Rest": [
      {
        "label": "Service",
        "value": "Recover and prepare",
        "kind": "fact"
      }
    ],
    "Quest NPC": [
      {
        "label": "Conversation",
        "value": "A road beyond the town",
        "kind": "fact"
      },
      {
        "label": "Known status",
        "value": "New dialogue",
        "kind": "fact"
      }
    ]
  },
  "primary": "Open service",
  "references": [
    {
      "file": "src/ui/screens/rest.js",
      "symbol": "mountRest",
      "status": "Current source reference; proposed W0/W1 layout"
    },
    {
      "file": "src/ui/screens/smithServices.js",
      "symbol": "openMountService",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "proposal": "Unified town workspace is proposed; current services remain authoritative."
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1c: Character creation

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Show only the active category. Small portrait, compact choices, explicit paging for long collections, persistent footer; avoid page scrolling. Allow one active-pane scroll at extreme text sizes instead of clipping or shrinking controls.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Create character [Face]                    [×] │
├──────────────┬─────────────────────────────────┤
│ Class        │ Attributes       Remaining:3    │
│ Starting kit │ Stat [−]8[+]   Stat [−]8[+]     │
│ Attributes ● │ Stat [−]8[+]   Stat [−]8[+]     │
│ Review       │ Derived effects / blocker       │
├──────────────┴─────────────────────────────────┤
│ [Back]                          [Next / Begin] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1c.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1c.header` | 95vw | 10vh | W1c.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1c.header.title` | 77vw | 6vh | W1c.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1c.header.exit` | 8vw | 6vh | W1c.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1c.body` | 95vw | 70vh | W1c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1c.body.navigation` | 21.6vw | 66vh | W1c.body | top-left | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Left category rail |
| `W1c.body.activePane` | 65.9vw | 66vh | W1c.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1c.body.stageControls` | 31.95vw | 66vh | W1c.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1c.body.derivedSummary` | 31.95vw | 66vh | W1c.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1c.footer` | 95vw | 10vh | W1c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1c.footer.closeBack` | 44vw | 6vh | W1c.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1c.footer.primary` | 44vw | 6vh | W1c.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1c.footer.singleAction` | 90vw | 6vh | W1c.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Create character [Face]      [×] │
│ [Category ▾]                     │
├──────────────────────────────────┤
│ Attributes     Remaining:3       │
│ Stat            [−]8[+]          │
│ Stat            [−]8[+]          │
│ Stat            [−]8[+]          │
│ Derived effects / blocker        │
├──────────────────────────────────┤
│ [Back]            [Next / Begin] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1c.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1c.header` | 95vw | 10vh | W1c.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1c.header.title` | 77vw | 6vh | W1c.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1c.header.exit` | 8vw | 6vh | W1c.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1c.body` | 95vw | 70vh | W1c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1c.body.navigation` | 90vw | 6vh | W1c.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1c.body.activePane` | 90vw | 58vh | W1c.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1c.body.stageControls` | 90vw | 28vh | W1c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1c.body.derivedSummary` | 90vw | 28vh | W1c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1c.footer` | 95vw | 10vh | W1c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1c.footer.closeBack` | 44vw | 6vh | W1c.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1c.footer.primary` | 44vw | 6vh | W1c.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1c.footer.singleAction` | 90vw | 6vh | W1c.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Create character      [×] │
│ [Face]                    │
│ [Category ▾]              │
├───────────────────────────┤
│ Attributes                │
│ Remaining:3               │
│ Stat            [−]8[+]   │
│ Stat            [−]8[+]   │
│ Stat            [−]8[+]   │
│ Derived effects / blocker │
├───────────────────────────┤
│ [Back]     [Next / Begin] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1c.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1c.header` | 95vw | 10vh | W1c.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1c.header.title` | 77vw | 6vh | W1c.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1c.header.exit` | 8vw | 6vh | W1c.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1c.body` | 95vw | 70vh | W1c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1c.body.navigation` | 90vw | 6vh | W1c.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1c.body.activePane` | 90vw | 58vh | W1c.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1c.body.stageControls` | 90vw | 28vh | W1c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1c.body.derivedSummary` | 90vw | 28vh | W1c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1c.footer` | 95vw | 10vh | W1c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1c.footer.closeBack` | 44vw | 6vh | W1c.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1c.footer.primary` | 44vw | 6vh | W1c.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1c.footer.singleAction` | 90vw | 6vh | W1c.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Create character      [×] │
│ [Face]                    │
│ [Category ▾]              │
├───────────────────────────┤
│ Attributes                │
│ Remaining:3               │
│ Stat            [−]8[+]   │
│ Stat            [−]8[+]   │
│ Stat            [−]8[+]   │
│ Derived effects / blocker │
├───────────────────────────┤
│ [Back]     [Next / Begin] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1c.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1c.header` | 95vw | 10vh | W1c.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1c.header.title` | 77vw | 6vh | W1c.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1c.header.exit` | 8vw | 6vh | W1c.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1c.body` | 95vw | 70vh | W1c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1c.body.navigation` | 90vw | 6vh | W1c.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1c.body.activePane` | 90vw | 58vh | W1c.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1c.body.stageControls` | 90vw | 28vh | W1c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1c.body.derivedSummary` | 90vw | 28vh | W1c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1c.footer` | 95vw | 10vh | W1c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1c.footer.closeBack` | 44vw | 6vh | W1c.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1c.footer.primary` | 44vw | 6vh | W1c.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1c.footer.singleAction` | 90vw | 6vh | W1c.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: creationDefinitions, draftCharacter, proposedDestinationSlot
PARENT: W1
model.categories = AvailableCreationStagesWithPrerequisiteReasons()
model.header = TitleAndSmallPortrait(draftCharacter)
model.body = ProjectOnlyActiveStage(draftCharacter)
IF stage == attributes:
    DisplayBudgetAndDerivedEffectsFromDomainProjection()
    UseTwoColumnControlsIfTheyFitOtherwiseOneColumn()
IF stage has tooManyChoices: UseExplicitPagesPreservingSelection()
model.actions = BackAndNextOrBegin()
RenderWithW1(model); ReservePersistentInlineFooter()
ON editDraft: ValidateDraftChange(); ReprojectWithoutSavingRun()
ON Next: ValidateActiveStage(); SelectNextAvailableStage()
ON Back: RestorePreviousStageWithoutDiscardingDraft()
ON Begin:
    RevalidateWholeDraftAndDestinationSlot()
    RequestExistingReplacementConfirmationIfNeeded()
    CommitCreationOnceThroughDomainCommand()
Allow one active-pane scroll only when readable content cannot fit.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/customize.js:87](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/customize.js#L87) — Current owner checkout (including inspected local edits)
- [src/ui/components/statAllocationCard.js:66](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/statAllocationCard.js#L66) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3), [WC3a](wireframe-gallery.html#WC3a)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Class": [
      {
        "label": "Class",
        "value": "Vanguard",
        "kind": "choice"
      },
      {
        "label": "Class",
        "value": "Wayfarer",
        "kind": "choice"
      }
    ],
    "Starting kit": [
      {
        "label": "Kit",
        "value": "Blade and guard",
        "kind": "choice"
      },
      {
        "label": "Kit",
        "value": "Staff and focus",
        "kind": "choice"
      }
    ],
    "Attributes": [
      {
        "label": "Strength",
        "value": "8",
        "kind": "stepper"
      },
      {
        "label": "Constitution",
        "value": "10",
        "kind": "stepper"
      }
    ],
    "Review": [
      {
        "label": "Name",
        "value": "New character",
        "kind": "text"
      },
      {
        "label": "Class",
        "value": "Selected class",
        "kind": "fact"
      },
      {
        "label": "Kit",
        "value": "Selected starting kit",
        "kind": "fact"
      }
    ]
  },
  "primary": "Next",
  "references": [
    {
      "file": "src/ui/screens/customize.js",
      "symbol": "mountCustomize",
      "status": "Current source reference; proposed W0/W1 layout"
    },
    {
      "file": "src/ui/components/statAllocationCard.js",
      "symbol": "renderStatAllocationCard",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC3a",
  "categoryCards": {
    "Class": "WC3a",
    "Starting kit": "WC3b"
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1d: Shop

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Offers use a W1v body.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Merchant · Cinders                         [×] │
├──────────────┬─────────────────────────────────┤
│ Cards ●      │ [Offer] [Offer]                 │
│ Relics       │ Price with each offer           │
│ Flasks       │ Availability / reason           │
│ Services     │ Selected offer detail           │
│ Sell         │                                 │
├──────────────┴─────────────────────────────────┤
│ [Leave]                      [Selected action] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1d.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1d.header` | 95vw | 10vh | W1d.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1d.header.title` | 77vw | 6vh | W1d.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1d.header.exit` | 8vw | 6vh | W1d.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1d.body` | 95vw | 70vh | W1d.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1d.body.navigation` | 21.6vw | 66vh | W1d.body | top-left | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Left category rail |
| `W1d.body.activePane` | 65.9vw | 66vh | W1d.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1d.body.offers` | 31.95vw | 66vh | W1d.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1d.body.offerDetail` | 31.95vw | 66vh | W1d.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1d.footer` | 95vw | 10vh | W1d.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1d.footer.closeBack` | 44vw | 6vh | W1d.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1d.footer.primary` | 44vw | 6vh | W1d.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1d.footer.singleAction` | 90vw | 6vh | W1d.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Merchant · Cinders           [×] │
│ [Category ▾]                     │
├──────────────────────────────────┤
│ [Offer] [Offer]                  │
│ Price with each offer            │
│ Availability / reason            │
│ Selected offer detail            │
├──────────────────────────────────┤
│ [Leave]        [Selected action] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1d.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1d.header` | 95vw | 10vh | W1d.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1d.header.title` | 77vw | 6vh | W1d.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1d.header.exit` | 8vw | 6vh | W1d.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1d.body` | 95vw | 70vh | W1d.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1d.body.navigation` | 90vw | 6vh | W1d.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1d.body.activePane` | 90vw | 58vh | W1d.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1d.body.offers` | 90vw | 28vh | W1d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1d.body.offerDetail` | 90vw | 28vh | W1d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1d.footer` | 95vw | 10vh | W1d.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1d.footer.closeBack` | 44vw | 6vh | W1d.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1d.footer.primary` | 44vw | 6vh | W1d.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1d.footer.singleAction` | 90vw | 6vh | W1d.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Merchant · Cinders    [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ [Offer] [Offer]           │
│ Price with each offer     │
│ Availability / reason     │
│ Selected offer detail     │
├───────────────────────────┤
│ [Leave] [Selected action] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1d.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1d.header` | 95vw | 10vh | W1d.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1d.header.title` | 77vw | 6vh | W1d.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1d.header.exit` | 8vw | 6vh | W1d.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1d.body` | 95vw | 70vh | W1d.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1d.body.navigation` | 90vw | 6vh | W1d.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1d.body.activePane` | 90vw | 58vh | W1d.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1d.body.offers` | 90vw | 28vh | W1d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1d.body.offerDetail` | 90vw | 28vh | W1d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1d.footer` | 95vw | 10vh | W1d.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1d.footer.closeBack` | 44vw | 6vh | W1d.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1d.footer.primary` | 44vw | 6vh | W1d.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1d.footer.singleAction` | 90vw | 6vh | W1d.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Merchant · Cinders    [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ [Offer] [Offer]           │
│ Price with each offer     │
│ Availability / reason     │
│ Selected offer detail     │
├───────────────────────────┤
│ [Leave] [Selected action] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1d.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1d.header` | 95vw | 10vh | W1d.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1d.header.title` | 77vw | 6vh | W1d.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1d.header.exit` | 8vw | 6vh | W1d.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1d.body` | 95vw | 70vh | W1d.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1d.body.navigation` | 90vw | 6vh | W1d.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1d.body.activePane` | 90vw | 58vh | W1d.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1d.body.offers` | 90vw | 28vh | W1d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1d.body.offerDetail` | 90vw | 28vh | W1d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1d.footer` | 95vw | 10vh | W1d.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1d.footer.closeBack` | 44vw | 6vh | W1d.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1d.footer.primary` | 44vw | 6vh | W1d.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1d.footer.singleAction` | 90vw | 6vh | W1d.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: savedShopStock, runSnapshot, shopDefinitions, localSelection
PARENT: W1; BODY: W1v offer model
model.categories = ProjectAvailableShopCategoriesFromData()
model.header = MerchantAndCurrentCurrency()
model.body = ProjectOffersFor(activeCategory, savedShopStock)
model.actions = LeaveAndApplicableSelectedOfferAction()
RenderWithW1(model)
ON chooseOffer(stableRef): UpdateSelectionAndDomainPreview(stableRef)
ON Buy/Sell/Remove:
    DispatchRegisteredCommandWithStableRefAndRequestId()
    RevalidateFundsCapacityOwnershipAndStockAtCommit()
    RefreshAfterResult(); PreserveCategoryAndSelectionWhenStillValid()
Do not reroll stock or use stale array indexes as transaction identity.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/shop.js:65](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/shop.js#L65) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3), [WC2a1](wireframe-gallery.html#WC2a1)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Equipment": [
      {
        "label": "Iron blade",
        "value": "40 Cinders",
        "kind": "choice"
      }
    ],
    "Relics": [
      {
        "label": "Old seal",
        "value": "60 Cinders",
        "kind": "choice"
      }
    ],
    "Consumables": [
      {
        "label": "Healing draught",
        "value": "20 Cinders",
        "kind": "choice"
      }
    ]
  },
  "primary": "Review purchase",
  "references": [
    {
      "file": "src/ui/screens/shop.js",
      "symbol": "mountShop",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC2a1",
  "categoryCards": {
    "Weapons": "WC2a1",
    "Equipment": "WC2a1",
    "Armor": "WC2a2",
    "Relics": "WC2b",
    "Cards": "WC1a",
    "Consumables": "WC2c1"
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1e: Armoury

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Preserve saved category IDs; detailed selection uses W1n.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Armoury                                    [×] │
├──────────────┬─────────────────────────────────┤
│ Character    │ [Item choices]                  │
│ Inventory ●  │ [Selected item / portrait]      │
│ Hybrid       │ Stats / requirements            │
│              │ Equipment effects               │
├──────────────┴─────────────────────────────────┤
│ [Back]                    [Equip if available] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1e.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1e.header` | 95vw | 10vh | W1e.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1e.header.title` | 77vw | 6vh | W1e.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1e.header.exit` | 8vw | 6vh | W1e.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1e.body` | 95vw | 70vh | W1e.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1e.body.navigation` | 21.6vw | 66vh | W1e.body | top-left | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Left category rail |
| `W1e.body.activePane` | 65.9vw | 66vh | W1e.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1e.body.itemCollection` | 31.95vw | 66vh | W1e.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1e.body.equipmentDetail` | 31.95vw | 66vh | W1e.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1e.footer` | 95vw | 10vh | W1e.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1e.footer.closeBack` | 44vw | 6vh | W1e.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1e.footer.primary` | 44vw | 6vh | W1e.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1e.footer.singleAction` | 90vw | 6vh | W1e.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Armoury                      [×] │
│ [Category ▾]                     │
├──────────────────────────────────┤
│ [Item choices]                   │
│ [Selected item / portrait]       │
│ Stats / requirements             │
│ Equipment effects                │
├──────────────────────────────────┤
│ [Back]      [Equip if available] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1e.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1e.header` | 95vw | 10vh | W1e.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1e.header.title` | 77vw | 6vh | W1e.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1e.header.exit` | 8vw | 6vh | W1e.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1e.body` | 95vw | 70vh | W1e.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1e.body.navigation` | 90vw | 6vh | W1e.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1e.body.activePane` | 90vw | 58vh | W1e.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1e.body.itemCollection` | 90vw | 28vh | W1e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1e.body.equipmentDetail` | 90vw | 28vh | W1e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1e.footer` | 95vw | 10vh | W1e.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1e.footer.closeBack` | 44vw | 6vh | W1e.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1e.footer.primary` | 44vw | 6vh | W1e.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1e.footer.singleAction` | 90vw | 6vh | W1e.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Armoury               [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ [Item choices]            │
│ [Selected item /          │
│ portrait]                 │
│ Stats / requirements      │
│ Equipment effects         │
├───────────────────────────┤
│ [Back]            [Equip] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1e.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1e.header` | 95vw | 10vh | W1e.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1e.header.title` | 77vw | 6vh | W1e.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1e.header.exit` | 8vw | 6vh | W1e.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1e.body` | 95vw | 70vh | W1e.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1e.body.navigation` | 90vw | 6vh | W1e.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1e.body.activePane` | 90vw | 58vh | W1e.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1e.body.itemCollection` | 90vw | 28vh | W1e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1e.body.equipmentDetail` | 90vw | 28vh | W1e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1e.footer` | 95vw | 10vh | W1e.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1e.footer.closeBack` | 44vw | 6vh | W1e.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1e.footer.primary` | 44vw | 6vh | W1e.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1e.footer.singleAction` | 90vw | 6vh | W1e.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Armoury               [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ [Item choices]            │
│ [Selected item /          │
│ portrait]                 │
│ Stats / requirements      │
│ Equipment effects         │
├───────────────────────────┤
│ [Back]            [Equip] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1e.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1e.header` | 95vw | 10vh | W1e.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1e.header.title` | 77vw | 6vh | W1e.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1e.header.exit` | 8vw | 6vh | W1e.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1e.body` | 95vw | 70vh | W1e.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1e.body.navigation` | 90vw | 6vh | W1e.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1e.body.activePane` | 90vw | 58vh | W1e.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1e.body.itemCollection` | 90vw | 28vh | W1e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1e.body.equipmentDetail` | 90vw | 28vh | W1e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1e.footer` | 95vw | 10vh | W1e.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1e.footer.closeBack` | 44vw | 6vh | W1e.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1e.footer.primary` | 44vw | 6vh | W1e.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1e.footer.singleAction` | 90vw | 6vh | W1e.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: loadoutSnapshot, armouryDefinitions, savedViewPreference
PARENT: W1
model.categories = CharacterInventoryHybridWithExistingSavedIDs()
model.body = ProjectActiveArmouryPaneUsingExistingLoadoutQueries()
model.actions = BackAndApplicableEquipmentAction()
RenderWithW1(model)
ON view/pane change: PreserveLocalSelectionAndTraySizes()
ON selectItem: RenderW1nBodyInsideExistingShell()
ON equip/unequip/swap: DispatchExistingValidatedEquipmentCommand()
ON result: RefreshAffectedStatsGrantsAndReceiptsFromDomain()
Never copy loadout math or rename persisted grid/rack/hybrid IDs.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/equipment.js:297](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/equipment.js#L297) — Current owner checkout (including inspected local edits)
- [src/ui/models/ArmouryModels.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/ArmouryModels.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3), [WC2a1](wireframe-gallery.html#WC2a1)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Weapons": [
      {
        "label": "Iron blade",
        "value": "Equipped",
        "kind": "choice"
      }
    ],
    "Armor": [
      {
        "label": "Travel armor",
        "value": "Equipped",
        "kind": "choice"
      }
    ],
    "Relics": [
      {
        "label": "Old seal",
        "value": "Owned",
        "kind": "choice"
      }
    ],
    "Cards": [
      {
        "label": "Strike",
        "value": "Installed",
        "kind": "choice"
      }
    ]
  },
  "primary": "Inspect",
  "references": [
    {
      "file": "src/ui/screens/equipment.js",
      "symbol": "mountEquipment",
      "status": "Current source reference; proposed W0/W1 layout"
    },
    {
      "file": "src/ui/models/ArmouryModels.js",
      "symbol": "Armoury models",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC2a1"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1f: Compendium

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Categories are illustrative; use actual registered groups and discovery rules.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Compendium                                 [×] │
├──────────────┬─────────────────────────────────┤
│ Cards ●      │ [Entry list]                    │
│ Items        │ Selected entry                  │
│ Enemies      │ Known facts / description       │
├──────────────┴─────────────────────────────────┤
│ [                    Back                    ] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1f.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1f.header` | 95vw | 10vh | W1f.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1f.header.title` | 77vw | 6vh | W1f.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1f.header.exit` | 8vw | 6vh | W1f.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1f.body` | 95vw | 70vh | W1f.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1f.body.navigation` | 21.6vw | 66vh | W1f.body | top-left | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Left category rail |
| `W1f.body.activePane` | 65.9vw | 66vh | W1f.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1f.body.entryList` | 31.95vw | 66vh | W1f.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1f.body.entryDetail` | 31.95vw | 66vh | W1f.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1f.footer` | 95vw | 10vh | W1f.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1f.footer.closeBack` | 44vw | 6vh | W1f.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1f.footer.primary` | 44vw | 6vh | W1f.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1f.footer.singleAction` | 90vw | 6vh | W1f.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Compendium                   [×] │
│ [Category ▾]                     │
├──────────────────────────────────┤
│ [Entry list]                     │
│ Selected entry                   │
│ Known facts / description        │
├──────────────────────────────────┤
│ [             Back             ] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1f.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1f.header` | 95vw | 10vh | W1f.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1f.header.title` | 77vw | 6vh | W1f.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1f.header.exit` | 8vw | 6vh | W1f.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1f.body` | 95vw | 70vh | W1f.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1f.body.navigation` | 90vw | 6vh | W1f.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1f.body.activePane` | 90vw | 58vh | W1f.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1f.body.entryList` | 90vw | 28vh | W1f.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1f.body.entryDetail` | 90vw | 28vh | W1f.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1f.footer` | 95vw | 10vh | W1f.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1f.footer.closeBack` | 44vw | 6vh | W1f.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1f.footer.primary` | 44vw | 6vh | W1f.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1f.footer.singleAction` | 90vw | 6vh | W1f.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Compendium            [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ [Entry list]              │
│ Selected entry            │
│ Known facts / description │
├───────────────────────────┤
│ [         Back          ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1f.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1f.header` | 95vw | 10vh | W1f.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1f.header.title` | 77vw | 6vh | W1f.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1f.header.exit` | 8vw | 6vh | W1f.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1f.body` | 95vw | 70vh | W1f.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1f.body.navigation` | 90vw | 6vh | W1f.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1f.body.activePane` | 90vw | 58vh | W1f.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1f.body.entryList` | 90vw | 28vh | W1f.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1f.body.entryDetail` | 90vw | 28vh | W1f.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1f.footer` | 95vw | 10vh | W1f.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1f.footer.closeBack` | 44vw | 6vh | W1f.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1f.footer.primary` | 44vw | 6vh | W1f.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1f.footer.singleAction` | 90vw | 6vh | W1f.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Compendium            [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ [Entry list]              │
│ Selected entry            │
│ Known facts / description │
├───────────────────────────┤
│ [         Back          ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1f.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1f.header` | 95vw | 10vh | W1f.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1f.header.title` | 77vw | 6vh | W1f.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1f.header.exit` | 8vw | 6vh | W1f.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1f.body` | 95vw | 70vh | W1f.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1f.body.navigation` | 90vw | 6vh | W1f.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1f.body.activePane` | 90vw | 58vh | W1f.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1f.body.entryList` | 90vw | 28vh | W1f.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1f.body.entryDetail` | 90vw | 28vh | W1f.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1f.footer` | 95vw | 10vh | W1f.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1f.footer.closeBack` | 44vw | 6vh | W1f.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1f.footer.primary` | 44vw | 6vh | W1f.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1f.footer.singleAction` | 90vw | 6vh | W1f.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: contentRegistries, discoverySnapshot, categoryAndEntrySelection
PARENT: W1
model.categories = RegisteredCompendiumGroups()
model.body = KnownEntryListAndSelectedEntryProjection()
model.actions = BackOnlyWhenApplicable()
RenderWithW1(model)
ON selectCategory/entry: ChangeLocalSelection(); ReprojectKnownFacts()
ON inspect: UseSharedInspectionBodyOrModalAsNavigationRequires()
Do not reveal undiscovered information or mutate progression on inspection.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/compendium.js:207](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/compendium.js#L207) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Cards": [
      {
        "label": "Strike",
        "value": "Discovered",
        "kind": "choice"
      }
    ],
    "Combatants": [
      {
        "label": "Ashen Sentinel",
        "value": "Known",
        "kind": "choice"
      }
    ],
    "Items": [
      {
        "label": "Healing draught",
        "value": "Known",
        "kind": "choice"
      }
    ],
    "Rules": [
      {
        "label": "Guard",
        "value": "Known rule",
        "kind": "choice"
      }
    ]
  },
  "primary": null,
  "references": [
    {
      "file": "src/ui/screens/compendium.js",
      "symbol": "mountCompendium",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1g: Profile

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Bind only supported profile categories. Do not conflate profile deletion with run deletion.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Profile                                    [×] │
├──────────────┬─────────────────────────────────┤
│ {Category A} │ Current profile identity        │
│ ●            │                                 │
│ {Category B} │ Selected category records       │
│              │ Applicable status / actions     │
├──────────────┴─────────────────────────────────┤
│ [Back]                     [Applicable action] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1g.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1g.header` | 95vw | 10vh | W1g.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1g.header.title` | 77vw | 6vh | W1g.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1g.header.exit` | 8vw | 6vh | W1g.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1g.body` | 95vw | 70vh | W1g.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1g.body.navigation` | 21.6vw | 66vh | W1g.body | top-left | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Left category rail |
| `W1g.body.activePane` | 65.9vw | 66vh | W1g.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1g.body.profileIdentity` | 31.95vw | 66vh | W1g.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1g.body.categoryRecords` | 31.95vw | 66vh | W1g.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1g.footer` | 95vw | 10vh | W1g.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1g.footer.closeBack` | 44vw | 6vh | W1g.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1g.footer.primary` | 44vw | 6vh | W1g.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1g.footer.singleAction` | 90vw | 6vh | W1g.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Profile                      [×] │
│ [Category ▾]                     │
├──────────────────────────────────┤
│ Current profile identity         │
│ Selected category records        │
│ Applicable status / actions      │
├──────────────────────────────────┤
│ [Back]       [Applicable action] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1g.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1g.header` | 95vw | 10vh | W1g.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1g.header.title` | 77vw | 6vh | W1g.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1g.header.exit` | 8vw | 6vh | W1g.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1g.body` | 95vw | 70vh | W1g.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1g.body.navigation` | 90vw | 6vh | W1g.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1g.body.activePane` | 90vw | 58vh | W1g.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1g.body.profileIdentity` | 90vw | 28vh | W1g.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1g.body.categoryRecords` | 90vw | 28vh | W1g.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1g.footer` | 95vw | 10vh | W1g.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1g.footer.closeBack` | 44vw | 6vh | W1g.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1g.footer.primary` | 44vw | 6vh | W1g.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1g.footer.singleAction` | 90vw | 6vh | W1g.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Profile               [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ Current profile identity  │
│ Selected category records │
│ Applicable status /       │
│ actions                   │
├───────────────────────────┤
│ [Back]           [Action] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1g.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1g.header` | 95vw | 10vh | W1g.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1g.header.title` | 77vw | 6vh | W1g.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1g.header.exit` | 8vw | 6vh | W1g.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1g.body` | 95vw | 70vh | W1g.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1g.body.navigation` | 90vw | 6vh | W1g.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1g.body.activePane` | 90vw | 58vh | W1g.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1g.body.profileIdentity` | 90vw | 28vh | W1g.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1g.body.categoryRecords` | 90vw | 28vh | W1g.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1g.footer` | 95vw | 10vh | W1g.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1g.footer.closeBack` | 44vw | 6vh | W1g.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1g.footer.primary` | 44vw | 6vh | W1g.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1g.footer.singleAction` | 90vw | 6vh | W1g.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Profile               [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ Current profile identity  │
│ Selected category records │
│ Applicable status /       │
│ actions                   │
├───────────────────────────┤
│ [Back]           [Action] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1g.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1g.header` | 95vw | 10vh | W1g.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1g.header.title` | 77vw | 6vh | W1g.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1g.header.exit` | 8vw | 6vh | W1g.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1g.body` | 95vw | 70vh | W1g.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1g.body.navigation` | 90vw | 6vh | W1g.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1g.body.activePane` | 90vw | 58vh | W1g.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1g.body.profileIdentity` | 90vw | 28vh | W1g.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1g.body.categoryRecords` | 90vw | 28vh | W1g.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1g.footer` | 95vw | 10vh | W1g.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1g.footer.closeBack` | 44vw | 6vh | W1g.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1g.footer.primary` | 44vw | 6vh | W1g.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1g.footer.singleAction` | 90vw | 6vh | W1g.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: profileSnapshot, profileCapabilities, categorySelection
PARENT: W1
model.categories = ExistingSupportedProfileCategories()
model.body = ProfileIdentityAndActiveCategoryRecords()
model.actions = ProjectActualBackAndPrimaryCapabilities()
RenderWithW1(model)
IF one footer action: W0 stretches it full width
IF two: W0 keeps Back left and concise primary right, INLINE
ON profileAction(recordId): DispatchExistingProfileCommandAndPolicy()
ON result: RefreshProfileRecordsAndStatus()
Do not conflate archive/restore/profile actions with run-slot deletion.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/profileArchive.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/profileArchive.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Overview": [
      {
        "label": "Profile",
        "value": "Local adventurer",
        "kind": "fact"
      },
      {
        "label": "Climbs",
        "value": "12",
        "kind": "fact"
      },
      {
        "label": "Completed",
        "value": "3",
        "kind": "fact"
      }
    ],
    "History": [
      {
        "label": "Previous climb",
        "value": "Act 2 · defeated",
        "kind": "fact"
      }
    ],
    "Discoveries": [
      {
        "label": "Combatants",
        "value": "Known records only",
        "kind": "fact"
      }
    ],
    "Progression": [
      {
        "label": "Weapons and skills",
        "value": "Open proficiency workspace",
        "kind": "progression"
      }
    ]
  },
  "primary": null,
  "references": [
    {
      "file": "src/ui/screens/profileArchive.js",
      "symbol": "Profile archive",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1h: Discard / Exhaust viewer

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. One combined pile control opens this workspace.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Card piles                                 [×] │
├──────────────┬─────────────────────────────────┤
│ Discard ●    │ [Card] [Card] [Card]            │
│ Exhaust      │ Selected card detail            │
│              │ Count / empty state             │
├──────────────┴─────────────────────────────────┤
│ [                   Close                    ] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1h.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1h.header` | 95vw | 10vh | W1h.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1h.header.title` | 77vw | 6vh | W1h.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1h.header.exit` | 8vw | 6vh | W1h.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1h.body` | 95vw | 70vh | W1h.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1h.body.navigation` | 21.6vw | 66vh | W1h.body | top-left | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Left category rail |
| `W1h.body.activePane` | 65.9vw | 66vh | W1h.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1h.body.cardCollection` | 31.95vw | 66vh | W1h.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1h.body.cardDetail` | 31.95vw | 66vh | W1h.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1h.footer` | 95vw | 10vh | W1h.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1h.footer.closeBack` | 44vw | 6vh | W1h.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1h.footer.primary` | 44vw | 6vh | W1h.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1h.footer.singleAction` | 90vw | 6vh | W1h.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Card piles                   [×] │
│ [Category ▾]                     │
├──────────────────────────────────┤
│ [Card] [Card] [Card]             │
│ Selected card detail             │
│ Count / empty state              │
├──────────────────────────────────┤
│ [            Close             ] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1h.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1h.header` | 95vw | 10vh | W1h.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1h.header.title` | 77vw | 6vh | W1h.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1h.header.exit` | 8vw | 6vh | W1h.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1h.body` | 95vw | 70vh | W1h.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1h.body.navigation` | 90vw | 6vh | W1h.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1h.body.activePane` | 90vw | 58vh | W1h.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1h.body.cardCollection` | 90vw | 28vh | W1h.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1h.body.cardDetail` | 90vw | 28vh | W1h.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1h.footer` | 95vw | 10vh | W1h.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1h.footer.closeBack` | 44vw | 6vh | W1h.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1h.footer.primary` | 44vw | 6vh | W1h.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1h.footer.singleAction` | 90vw | 6vh | W1h.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Card piles            [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ [Card] [Card] [Card]      │
│ Selected card detail      │
│ Count / empty state       │
├───────────────────────────┤
│ [         Close         ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1h.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1h.header` | 95vw | 10vh | W1h.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1h.header.title` | 77vw | 6vh | W1h.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1h.header.exit` | 8vw | 6vh | W1h.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1h.body` | 95vw | 70vh | W1h.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1h.body.navigation` | 90vw | 6vh | W1h.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1h.body.activePane` | 90vw | 58vh | W1h.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1h.body.cardCollection` | 90vw | 28vh | W1h.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1h.body.cardDetail` | 90vw | 28vh | W1h.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1h.footer` | 95vw | 10vh | W1h.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1h.footer.closeBack` | 44vw | 6vh | W1h.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1h.footer.primary` | 44vw | 6vh | W1h.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1h.footer.singleAction` | 90vw | 6vh | W1h.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Card piles            [×] │
│ [Category ▾]              │
├───────────────────────────┤
│ [Card] [Card] [Card]      │
│ Selected card detail      │
│ Count / empty state       │
├───────────────────────────┤
│ [         Close         ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1h.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1h.header` | 95vw | 10vh | W1h.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1h.header.title` | 77vw | 6vh | W1h.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1h.header.exit` | 8vw | 6vh | W1h.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1h.body` | 95vw | 70vh | W1h.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1h.body.navigation` | 90vw | 6vh | W1h.body | top / full usable width | start / start | normal grid flow | 2.5vw from left; 2vh from body top | Selector above active pane; consumes body budget |
| `W1h.body.activePane` | 90vw | 58vh | W1h.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1h.body.cardCollection` | 90vw | 28vh | W1h.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1h.body.cardDetail` | 90vw | 28vh | W1h.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1h.footer` | 95vw | 10vh | W1h.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1h.footer.closeBack` | 44vw | 6vh | W1h.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1h.footer.primary` | 44vw | 6vh | W1h.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1h.footer.singleAction` | 90vw | 6vh | W1h.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: committedCombatSnapshot, selectedPile, selectedCardRef
PARENT: W1
model.categories = DiscardAndExhaustWithCounts()
model.body = ProjectCardsIn(selectedPile)
model.actions = CloseIfApplicable()
RenderWithW1(model)
ON selectPile: PreserveCardSelectionIfStillPresent(); update body
ON selectCard: ProjectReadOnlyCardDetails()
ON combatSnapshotChange: RefreshCountsAndInvalidateMissingSelection()
No draw, discard, exhaust, or turn mutation occurs by opening this viewer.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/piles.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/piles.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WC1a](wireframe-gallery.html#WC1a)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Discard": [
      {
        "label": "Strike",
        "value": "Discarded",
        "kind": "choice"
      }
    ],
    "Exhaust": [
      {
        "label": "Last stand",
        "value": "Exhausted",
        "kind": "choice"
      }
    ]
  },
  "primary": null,
  "references": [
    {
      "file": "src/ui/components/piles.js",
      "symbol": "Piles viewer",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC1a"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1i: Smith upgrade

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Upgrade equipment                          [×] │
├──────────────────┬─────────────────────────────┤
│ ○ Item A         │ Selected item               │
│ ● Item B         │ Current → Proposed          │
│                  │ Stat changes / requirements │
│                  │ Cost / available stones     │
├──────────────────┴─────────────────────────────┤
│ [Back]                               [Upgrade] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1i.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1i.header` | 95vw | 10vh | W1i.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1i.header.title` | 77vw | 6vh | W1i.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1i.header.exit` | 8vw | 6vh | W1i.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1i.body` | 95vw | 70vh | W1i.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1i.body.activePane` | 90vw | 66vh | W1i.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1i.body.candidates` | 44vw | 66vh | W1i.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1i.body.upgradePreview` | 44vw | 66vh | W1i.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1i.footer` | 95vw | 10vh | W1i.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1i.footer.closeBack` | 44vw | 6vh | W1i.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1i.footer.primary` | 44vw | 6vh | W1i.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1i.footer.singleAction` | 90vw | 6vh | W1i.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Upgrade equipment            [×] │
│ [Selection ▾]                    │
│ Selected item                    │
│ Current → Proposed               │
│ Stat changes / requirements      │
│ Cost / available stones          │
├──────────────────────────────────┤
│ [Back]                 [Upgrade] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1i.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1i.header` | 95vw | 10vh | W1i.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1i.header.title` | 77vw | 6vh | W1i.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1i.header.exit` | 8vw | 6vh | W1i.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1i.body` | 95vw | 70vh | W1i.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1i.body.activePane` | 90vw | 66vh | W1i.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1i.body.candidates` | 90vw | 32vh | W1i.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1i.body.upgradePreview` | 90vw | 32vh | W1i.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1i.footer` | 95vw | 10vh | W1i.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1i.footer.closeBack` | 44vw | 6vh | W1i.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1i.footer.primary` | 44vw | 6vh | W1i.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1i.footer.singleAction` | 90vw | 6vh | W1i.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Upgrade equipment     [×] │
│ [Selection ▾]             │
│                           │
│ Selected item             │
│ Current → Proposed        │
│ Stat changes /            │
│ requirements              │
│ Cost / available stones   │
│                           │
├───────────────────────────┤
│ [Back]          [Upgrade] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1i.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1i.header` | 95vw | 10vh | W1i.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1i.header.title` | 77vw | 6vh | W1i.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1i.header.exit` | 8vw | 6vh | W1i.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1i.body` | 95vw | 70vh | W1i.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1i.body.activePane` | 90vw | 66vh | W1i.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1i.body.candidates` | 90vw | 32vh | W1i.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1i.body.upgradePreview` | 90vw | 32vh | W1i.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1i.footer` | 95vw | 10vh | W1i.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1i.footer.closeBack` | 44vw | 6vh | W1i.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1i.footer.primary` | 44vw | 6vh | W1i.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1i.footer.singleAction` | 90vw | 6vh | W1i.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Upgrade equipment     [×] │
│ [Selection ▾]             │
│                           │
│ Selected item             │
│ Current → Proposed        │
│ Stat changes /            │
│ requirements              │
│ Cost / available stones   │
│                           │
├───────────────────────────┤
│ [Back]          [Upgrade] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1i.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1i.header` | 95vw | 10vh | W1i.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1i.header.title` | 77vw | 6vh | W1i.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1i.header.exit` | 8vw | 6vh | W1i.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1i.body` | 95vw | 70vh | W1i.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1i.body.activePane` | 90vw | 66vh | W1i.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1i.body.candidates` | 90vw | 32vh | W1i.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1i.body.upgradePreview` | 90vw | 32vh | W1i.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1i.footer` | 95vw | 10vh | W1i.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1i.footer.closeBack` | 44vw | 6vh | W1i.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1i.footer.primary` | 44vw | 6vh | W1i.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1i.footer.singleAction` | 90vw | 6vh | W1i.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: serviceDefinition, runSnapshot, selectedItemRef
PARENT: W1; BODY: selection/detail
plan = ExistingSmithingPlan(runSnapshot)
model.body = CandidateRowsAndUpgradeDelta(plan, selectedItemRef)
model.actions = BackAndUpgrade(plan.canConfirm, plan.reason)
RenderWithW1(model)
ON selectItem(ref): SetLocalSelection(ref); RecomputePlanAndPreview()
ON Upgrade: DispatchExistingUpgradeCommand(ref, reviewedConsequence, requestId)
IF stale/rejected: KeepOpenAndRefreshReason()
IF committed: FollowConfiguredExistingCloseOrMultiUseContinuation()
Cancel must leave domain state unchanged.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/smithUpgradeModal.js:58](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/smithUpgradeModal.js#L58) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3), [WC2a1](wireframe-gallery.html#WC2a1)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Equipment": [
      {
        "label": "Iron blade",
        "value": "Eligible",
        "kind": "choice"
      }
    ],
    "Preview": [
      {
        "label": "Current",
        "value": "Base equipment",
        "kind": "fact"
      },
      {
        "label": "After",
        "value": "Projected upgrade",
        "kind": "fact"
      },
      {
        "label": "Cost",
        "value": "Quoted by service model",
        "kind": "fact"
      }
    ]
  },
  "primary": "Review upgrade",
  "references": [
    {
      "file": "src/ui/components/smithUpgradeModal.js",
      "symbol": "mountSmithUpgradeModal",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC2a1"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1j: Extract card

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. A registered mount-selector slot supplies the extra step.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Extract card                               [×] │
├──────────────────┬─────────────────────────────┤
│ ○ Item A         │ Selected item               │
│ ● Item B         │ [Mount selector]            │
│                  │ Card / fallback preview     │
│                  │ Cost / available stones     │
├──────────────────┴─────────────────────────────┤
│ [Back]                               [Extract] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1j.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1j.header` | 95vw | 10vh | W1j.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1j.header.title` | 77vw | 6vh | W1j.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1j.header.exit` | 8vw | 6vh | W1j.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1j.body` | 95vw | 70vh | W1j.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1j.body.activePane` | 90vw | 66vh | W1j.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1j.body.items` | 90vw | 20.67vh | W1j.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1j.body.mounts` | 90vw | 20.67vh | W1j.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1j.body.extractionPreview` | 90vw | 20.67vh | W1j.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1j.footer` | 95vw | 10vh | W1j.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1j.footer.closeBack` | 44vw | 6vh | W1j.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1j.footer.primary` | 44vw | 6vh | W1j.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1j.footer.singleAction` | 90vw | 6vh | W1j.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Extract card                 [×] │
│ [Selection ▾]                    │
│ Selected item                    │
│ [Mount selector]                 │
│ Card / fallback preview          │
│ Cost / available stones          │
├──────────────────────────────────┤
│ [Back]                 [Extract] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1j.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1j.header` | 95vw | 10vh | W1j.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1j.header.title` | 77vw | 6vh | W1j.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1j.header.exit` | 8vw | 6vh | W1j.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1j.body` | 95vw | 70vh | W1j.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1j.body.activePane` | 90vw | 66vh | W1j.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1j.body.items` | 90vw | 20.67vh | W1j.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1j.body.mounts` | 90vw | 20.67vh | W1j.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1j.body.extractionPreview` | 90vw | 20.67vh | W1j.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1j.footer` | 95vw | 10vh | W1j.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1j.footer.closeBack` | 44vw | 6vh | W1j.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1j.footer.primary` | 44vw | 6vh | W1j.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1j.footer.singleAction` | 90vw | 6vh | W1j.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Extract card          [×] │
│ [Selection ▾]             │
│                           │
│ Selected item             │
│ [Mount selector]          │
│ Card / fallback preview   │
│ Cost / available stones   │
│                           │
├───────────────────────────┤
│ [Back]          [Extract] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1j.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1j.header` | 95vw | 10vh | W1j.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1j.header.title` | 77vw | 6vh | W1j.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1j.header.exit` | 8vw | 6vh | W1j.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1j.body` | 95vw | 70vh | W1j.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1j.body.activePane` | 90vw | 66vh | W1j.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1j.body.items` | 90vw | 20.67vh | W1j.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1j.body.mounts` | 90vw | 20.67vh | W1j.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1j.body.extractionPreview` | 90vw | 20.67vh | W1j.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1j.footer` | 95vw | 10vh | W1j.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1j.footer.closeBack` | 44vw | 6vh | W1j.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1j.footer.primary` | 44vw | 6vh | W1j.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1j.footer.singleAction` | 90vw | 6vh | W1j.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Extract card          [×] │
│ [Selection ▾]             │
│                           │
│ Selected item             │
│ [Mount selector]          │
│ Card / fallback preview   │
│ Cost / available stones   │
│                           │
├───────────────────────────┤
│ [Back]          [Extract] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1j.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1j.header` | 95vw | 10vh | W1j.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1j.header.title` | 77vw | 6vh | W1j.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1j.header.exit` | 8vw | 6vh | W1j.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1j.body` | 95vw | 70vh | W1j.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1j.body.activePane` | 90vw | 66vh | W1j.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1j.body.items` | 90vw | 20.67vh | W1j.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1j.body.mounts` | 90vw | 20.67vh | W1j.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1j.body.extractionPreview` | 90vw | 20.67vh | W1j.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1j.footer` | 95vw | 10vh | W1j.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1j.footer.closeBack` | 44vw | 6vh | W1j.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1j.footer.primary` | 44vw | 6vh | W1j.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1j.footer.singleAction` | 90vw | 6vh | W1j.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: extractionDefinition, runSnapshot, itemSelection, mountSelection
PARENT: W1; BODY: selection/detail with mount selector
plan = ExistingExtractionPlan(runSnapshot)
model.body = ItemsMountsCardAndFallbackPreview(plan, selections)
model.actions = BackAndExtractWithEligibility()
RenderWithW1(model)
ON selectItem: ClearMountSelection(); Reproject()
ON selectMount: SetLocalMountSelection(); Reproject()
ON Extract: DispatchExistingExtractionCommand(itemRef, mountKey, requestId)
ON success: RefreshReceiptAndConfiguredContinuation()
Back/Close never extracts or changes a fallback card.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/smithServices.js:26](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/smithServices.js#L26) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3), [WC1a](wireframe-gallery.html#WC1a)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Equipment": [
      {
        "label": "Iron blade",
        "value": "Select source",
        "kind": "choice"
      }
    ],
    "Cards": [
      {
        "label": "Strike",
        "value": "Installed",
        "kind": "choice"
      }
    ]
  },
  "primary": "Review extraction",
  "references": [
    {
      "file": "src/ui/screens/smithServices.js",
      "symbol": "openMountService",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC1a"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1k: Install card

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Clear dependent mount/card selections when the parent changes.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Install card                               [×] │
├──────────────────┬─────────────────────────────┤
│ ○ Item A         │ Selected item               │
│ ● Item B         │ [Mount selector]            │
│                  │ [Compatible card selector]  │
│                  │ Cost / available stones     │
├──────────────────┴─────────────────────────────┤
│ [Back]                               [Install] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1k.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1k.header` | 95vw | 10vh | W1k.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1k.header.title` | 77vw | 6vh | W1k.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1k.header.exit` | 8vw | 6vh | W1k.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1k.body` | 95vw | 70vh | W1k.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1k.body.activePane` | 90vw | 66vh | W1k.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1k.body.items` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.body.mounts` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.body.cards` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.body.installPreview` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.footer` | 95vw | 10vh | W1k.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1k.footer.closeBack` | 44vw | 6vh | W1k.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1k.footer.primary` | 44vw | 6vh | W1k.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1k.footer.singleAction` | 90vw | 6vh | W1k.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Install card                 [×] │
│ [Selection ▾]                    │
│ Selected item                    │
│ [Mount selector]                 │
│ [Compatible card selector]       │
│ Cost / available stones          │
├──────────────────────────────────┤
│ [Back]                 [Install] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1k.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1k.header` | 95vw | 10vh | W1k.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1k.header.title` | 77vw | 6vh | W1k.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1k.header.exit` | 8vw | 6vh | W1k.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1k.body` | 95vw | 70vh | W1k.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1k.body.activePane` | 90vw | 66vh | W1k.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1k.body.items` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.body.mounts` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.body.cards` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.body.installPreview` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.footer` | 95vw | 10vh | W1k.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1k.footer.closeBack` | 44vw | 6vh | W1k.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1k.footer.primary` | 44vw | 6vh | W1k.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1k.footer.singleAction` | 90vw | 6vh | W1k.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Install card          [×] │
│ [Selection ▾]             │
│                           │
│ Selected item             │
│ [Mount selector]          │
│ [Compatible card          │
│ selector]                 │
│ Cost / available stones   │
│                           │
├───────────────────────────┤
│ [Back]          [Install] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1k.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1k.header` | 95vw | 10vh | W1k.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1k.header.title` | 77vw | 6vh | W1k.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1k.header.exit` | 8vw | 6vh | W1k.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1k.body` | 95vw | 70vh | W1k.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1k.body.activePane` | 90vw | 66vh | W1k.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1k.body.items` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.body.mounts` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.body.cards` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.body.installPreview` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.footer` | 95vw | 10vh | W1k.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1k.footer.closeBack` | 44vw | 6vh | W1k.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1k.footer.primary` | 44vw | 6vh | W1k.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1k.footer.singleAction` | 90vw | 6vh | W1k.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Install card          [×] │
│ [Selection ▾]             │
│                           │
│ Selected item             │
│ [Mount selector]          │
│ [Compatible card          │
│ selector]                 │
│ Cost / available stones   │
│                           │
├───────────────────────────┤
│ [Back]          [Install] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1k.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1k.header` | 95vw | 10vh | W1k.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1k.header.title` | 77vw | 6vh | W1k.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1k.header.exit` | 8vw | 6vh | W1k.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1k.body` | 95vw | 70vh | W1k.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1k.body.activePane` | 90vw | 66vh | W1k.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1k.body.items` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.body.mounts` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.body.cards` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.body.installPreview` | 90vw | 15vh | W1k.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1k.footer` | 95vw | 10vh | W1k.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1k.footer.closeBack` | 44vw | 6vh | W1k.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1k.footer.primary` | 44vw | 6vh | W1k.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1k.footer.singleAction` | 90vw | 6vh | W1k.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: installDefinition, runSnapshot, itemMountCardSelection
PARENT: W1; BODY: selection/detail with dependent selectors
plan = ExistingInstallPlan(runSnapshot)
model.body = ItemsMountsCompatibleCardsAndCost(plan, selection)
model.actions = BackAndInstallWithEligibility()
RenderWithW1(model)
ON selectItem: ClearMountAndCardSelection(); Reproject()
ON selectMount: ClearCardSelection(); Reproject()
ON selectCard(instanceId): ValidateLocalCandidate(); Reproject()
ON Install: DispatchExistingInstallCommand(itemRef, mountKey, instanceId, requestId)
ON rejection: PreserveValidSelectionsAndShowCurrentReason()
Do not change card ownership until the command commits.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/smithServices.js:26](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/smithServices.js#L26) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3), [WC1a](wireframe-gallery.html#WC1a)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Cards": [
      {
        "label": "Strike",
        "value": "Owned",
        "kind": "choice"
      }
    ],
    "Equipment": [
      {
        "label": "Iron blade",
        "value": "Compatible",
        "kind": "choice"
      }
    ]
  },
  "primary": "Review installation",
  "references": [
    {
      "file": "src/ui/screens/smithServices.js",
      "symbol": "openMountService",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC1a"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1l: New game slot selection

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Same SaveSlotSelectionViewModel with a mode; New permits empty slots, Load does not. Selecting does not overwrite storage.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ New game                                   [×] │
│                                                │
│ ● Slot 1 · Identity / location                 │
│ ○ Slot 2 · Identity / location                 │
│ ○ Slot 3 · Empty                               │
│ [Delete selected, if offered]                  │
├────────────────────────────────────────────────┤
│ [Back]                      [Create character] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1l.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1l.header` | 95vw | 10vh | W1l.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1l.header.title` | 77vw | 6vh | W1l.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1l.header.exit` | 8vw | 6vh | W1l.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1l.body` | 95vw | 70vh | W1l.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1l.body.activePane` | 90vw | 66vh | W1l.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1l.body.slotList` | 90vw | 66vh | W1l.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1l.footer` | 95vw | 10vh | W1l.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1l.footer.closeBack` | 44vw | 6vh | W1l.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1l.footer.primary` | 44vw | 6vh | W1l.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1l.footer.singleAction` | 90vw | 6vh | W1l.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ New game                     [×] │
│ ● Slot 1 · Identity / location   │
│ ○ Slot 2 · Identity / location   │
│ ○ Slot 3 · Empty                 │
│ [Delete selected, if offered]    │
├──────────────────────────────────┤
│ [Back]        [Create character] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1l.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1l.header` | 95vw | 10vh | W1l.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1l.header.title` | 77vw | 6vh | W1l.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1l.header.exit` | 8vw | 6vh | W1l.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1l.body` | 95vw | 70vh | W1l.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1l.body.activePane` | 90vw | 66vh | W1l.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1l.body.slotList` | 90vw | 66vh | W1l.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1l.footer` | 95vw | 10vh | W1l.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1l.footer.closeBack` | 44vw | 6vh | W1l.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1l.footer.primary` | 44vw | 6vh | W1l.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1l.footer.singleAction` | 90vw | 6vh | W1l.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ New game              [×] │
│                           │
│ ● Slot 1 · Identity /     │
│ location                  │
│ ○ Slot 2 · Identity /     │
│ location                  │
│ ○ Slot 3 · Empty          │
│ [Delete selected, if      │
│ offered]                  │
│                           │
├───────────────────────────┤
│ [Back] [Create character] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1l.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1l.header` | 95vw | 10vh | W1l.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1l.header.title` | 77vw | 6vh | W1l.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1l.header.exit` | 8vw | 6vh | W1l.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1l.body` | 95vw | 70vh | W1l.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1l.body.activePane` | 90vw | 66vh | W1l.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1l.body.slotList` | 90vw | 66vh | W1l.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1l.footer` | 95vw | 10vh | W1l.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1l.footer.closeBack` | 44vw | 6vh | W1l.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1l.footer.primary` | 44vw | 6vh | W1l.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1l.footer.singleAction` | 90vw | 6vh | W1l.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ New game              [×] │
│                           │
│ ● Slot 1 · Identity /     │
│ location                  │
│ ○ Slot 2 · Identity /     │
│ location                  │
│ ○ Slot 3 · Empty          │
│ [Delete selected, if      │
│ offered]                  │
│                           │
├───────────────────────────┤
│ [Back] [Create character] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1l.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1l.header` | 95vw | 10vh | W1l.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1l.header.title` | 77vw | 6vh | W1l.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1l.header.exit` | 8vw | 6vh | W1l.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1l.body` | 95vw | 70vh | W1l.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1l.body.activePane` | 90vw | 66vh | W1l.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1l.body.slotList` | 90vw | 66vh | W1l.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1l.footer` | 95vw | 10vh | W1l.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1l.footer.closeBack` | 44vw | 6vh | W1l.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1l.footer.primary` | 44vw | 6vh | W1l.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1l.footer.singleAction` | 90vw | 6vh | W1l.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: saveSlotSummaries, selectedSlot, draftCreationContext
PARENT: W1; BODY: shared SaveSlotSelection(mode = new)
model.body = SlotsWithEmptyAndOccupiedStates()
model.actions = BackAndCreateCharacter(selectedSlotIsValid)
RenderWithW1(model)
ON selectSlot(id): StoreProposedDestinationOnly()
ON CreateCharacter: OpenW1cWithDraftAndProposedDestination()
ON cancel: ReturnWithoutDeletingOrOverwritingAnySlot()
Occupied-slot replacement follows W2c at the approved write boundary.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/saveSlotSelector.js:9](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/saveSlotSelector.js#L9) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Save slots": [
      {
        "label": "Slot 1",
        "value": "Vanguard · Act 1 · Floor 3",
        "kind": "slot"
      },
      {
        "label": "Slot 2",
        "value": "Empty · start a climb",
        "kind": "slot"
      },
      {
        "label": "Slot 3",
        "value": "Empty · start a climb",
        "kind": "slot"
      }
    ]
  },
  "primary": "Continue",
  "references": [
    {
      "file": "src/ui/components/saveSlotSelector.js",
      "symbol": "slotDoor",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1m: Load game slot selection

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Same SaveSlotSelectionViewModel with a mode; New permits empty slots, Load does not. Selecting does not overwrite storage.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Load game                                  [×] │
│                                                │
│ ● Slot 1 · Identity / location                 │
│ ○ Slot 2 · Identity / location                 │
│ ○ Slot 3 · Empty                               │
│ [Delete selected, if offered]                  │
├────────────────────────────────────────────────┤
│ [Back]                                  [Load] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1m.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1m.header` | 95vw | 10vh | W1m.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1m.header.title` | 77vw | 6vh | W1m.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1m.header.exit` | 8vw | 6vh | W1m.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1m.body` | 95vw | 70vh | W1m.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1m.body.activePane` | 90vw | 66vh | W1m.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1m.body.slotList` | 90vw | 66vh | W1m.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1m.footer` | 95vw | 10vh | W1m.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1m.footer.closeBack` | 44vw | 6vh | W1m.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1m.footer.primary` | 44vw | 6vh | W1m.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1m.footer.singleAction` | 90vw | 6vh | W1m.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Load game                    [×] │
│ ● Slot 1 · Identity / location   │
│ ○ Slot 2 · Identity / location   │
│ ○ Slot 3 · Empty                 │
│ [Delete selected, if offered]    │
├──────────────────────────────────┤
│ [Back]                    [Load] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1m.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1m.header` | 95vw | 10vh | W1m.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1m.header.title` | 77vw | 6vh | W1m.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1m.header.exit` | 8vw | 6vh | W1m.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1m.body` | 95vw | 70vh | W1m.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1m.body.activePane` | 90vw | 66vh | W1m.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1m.body.slotList` | 90vw | 66vh | W1m.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1m.footer` | 95vw | 10vh | W1m.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1m.footer.closeBack` | 44vw | 6vh | W1m.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1m.footer.primary` | 44vw | 6vh | W1m.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1m.footer.singleAction` | 90vw | 6vh | W1m.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Load game             [×] │
│                           │
│ ● Slot 1 · Identity /     │
│ location                  │
│ ○ Slot 2 · Identity /     │
│ location                  │
│ ○ Slot 3 · Empty          │
│ [Delete selected, if      │
│ offered]                  │
│                           │
├───────────────────────────┤
│ [Back]             [Load] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1m.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1m.header` | 95vw | 10vh | W1m.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1m.header.title` | 77vw | 6vh | W1m.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1m.header.exit` | 8vw | 6vh | W1m.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1m.body` | 95vw | 70vh | W1m.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1m.body.activePane` | 90vw | 66vh | W1m.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1m.body.slotList` | 90vw | 66vh | W1m.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1m.footer` | 95vw | 10vh | W1m.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1m.footer.closeBack` | 44vw | 6vh | W1m.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1m.footer.primary` | 44vw | 6vh | W1m.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1m.footer.singleAction` | 90vw | 6vh | W1m.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Load game             [×] │
│                           │
│ ● Slot 1 · Identity /     │
│ location                  │
│ ○ Slot 2 · Identity /     │
│ location                  │
│ ○ Slot 3 · Empty          │
│ [Delete selected, if      │
│ offered]                  │
│                           │
├───────────────────────────┤
│ [Back]             [Load] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1m.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1m.header` | 95vw | 10vh | W1m.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1m.header.title` | 77vw | 6vh | W1m.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1m.header.exit` | 8vw | 6vh | W1m.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1m.body` | 95vw | 70vh | W1m.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1m.body.activePane` | 90vw | 66vh | W1m.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1m.body.slotList` | 90vw | 66vh | W1m.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1m.footer` | 95vw | 10vh | W1m.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1m.footer.closeBack` | 44vw | 6vh | W1m.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1m.footer.primary` | 44vw | 6vh | W1m.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1m.footer.singleAction` | 90vw | 6vh | W1m.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: saveSlotSummaries, selectedSlot, activeRunContext
PARENT: W1; BODY: shared SaveSlotSelection(mode = load)
model.body = SlotsWithLoadabilityAndReasons()
model.actions = BackAndLoad(selectedSlotCanLoad)
RenderWithW1(model)
ON selectSlot: UpdateLocalSelectionOnly()
ON Load:
    RefreshAndValidateSelectedSaveIdentity()
    IF activeProgressRequiresDecision: OpenW2dWithExactTarget()
    ELSE DispatchExistingLoadCommand()
ON DeleteSelected: OpenW2b; never delete on ordinary selection
Empty/unreadable/incompatible slots cannot load.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/saveSlotSelector.js:228](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/saveSlotSelector.js#L228) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Save slots": [
      {
        "label": "Slot 1",
        "value": "Vanguard · Act 1 · Floor 3",
        "kind": "slot"
      },
      {
        "label": "Slot 2",
        "value": "Wayfarer · Act 2 · Floor 1",
        "kind": "slot"
      },
      {
        "label": "Slot 3",
        "value": "No saved climb",
        "kind": "disabled"
      }
    ]
  },
  "primary": "Load selected",
  "references": [
    {
      "file": "src/ui/components/saveSlotSelector.js",
      "symbol": "openSaveSlotSelector",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1n: Inventory selection

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. This body can live inside W1e; render one shell, not two.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Inventory                                  [×] │
├──────────────────┬─────────────────────────────┤
│ ○ Item A         │ Selected item               │
│ ● Item B         │ Stats / requirements        │
│                  │ Compared with equipped      │
│                  │ Eligibility / blocker       │
├──────────────────┴─────────────────────────────┤
│ [Back]                                 [Equip] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1n.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1n.header` | 95vw | 10vh | W1n.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1n.header.title` | 77vw | 6vh | W1n.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1n.header.exit` | 8vw | 6vh | W1n.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1n.body` | 95vw | 70vh | W1n.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1n.body.activePane` | 90vw | 66vh | W1n.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1n.body.items` | 44vw | 66vh | W1n.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1n.body.comparison` | 44vw | 66vh | W1n.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1n.footer` | 95vw | 10vh | W1n.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1n.footer.closeBack` | 44vw | 6vh | W1n.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1n.footer.primary` | 44vw | 6vh | W1n.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1n.footer.singleAction` | 90vw | 6vh | W1n.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Inventory                    [×] │
│ [Selection ▾]                    │
│ Selected item                    │
│ Stats / requirements             │
│ Compared with equipped           │
│ Eligibility / blocker            │
├──────────────────────────────────┤
│ [Back]                   [Equip] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1n.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1n.header` | 95vw | 10vh | W1n.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1n.header.title` | 77vw | 6vh | W1n.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1n.header.exit` | 8vw | 6vh | W1n.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1n.body` | 95vw | 70vh | W1n.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1n.body.activePane` | 90vw | 66vh | W1n.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1n.body.items` | 90vw | 32vh | W1n.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1n.body.comparison` | 90vw | 32vh | W1n.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1n.footer` | 95vw | 10vh | W1n.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1n.footer.closeBack` | 44vw | 6vh | W1n.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1n.footer.primary` | 44vw | 6vh | W1n.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1n.footer.singleAction` | 90vw | 6vh | W1n.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Inventory             [×] │
│ [Selection ▾]             │
│                           │
│ Selected item             │
│ Stats / requirements      │
│ Compared with equipped    │
│ Eligibility / blocker     │
│                           │
├───────────────────────────┤
│ [Back]            [Equip] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1n.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1n.header` | 95vw | 10vh | W1n.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1n.header.title` | 77vw | 6vh | W1n.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1n.header.exit` | 8vw | 6vh | W1n.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1n.body` | 95vw | 70vh | W1n.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1n.body.activePane` | 90vw | 66vh | W1n.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1n.body.items` | 90vw | 32vh | W1n.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1n.body.comparison` | 90vw | 32vh | W1n.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1n.footer` | 95vw | 10vh | W1n.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1n.footer.closeBack` | 44vw | 6vh | W1n.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1n.footer.primary` | 44vw | 6vh | W1n.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1n.footer.singleAction` | 90vw | 6vh | W1n.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Inventory             [×] │
│ [Selection ▾]             │
│                           │
│ Selected item             │
│ Stats / requirements      │
│ Compared with equipped    │
│ Eligibility / blocker     │
│                           │
├───────────────────────────┤
│ [Back]            [Equip] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1n.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1n.header` | 95vw | 10vh | W1n.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1n.header.title` | 77vw | 6vh | W1n.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1n.header.exit` | 8vw | 6vh | W1n.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1n.body` | 95vw | 70vh | W1n.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1n.body.activePane` | 90vw | 66vh | W1n.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1n.body.items` | 90vw | 32vh | W1n.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1n.body.comparison` | 90vw | 32vh | W1n.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1n.footer` | 95vw | 10vh | W1n.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1n.footer.closeBack` | 44vw | 6vh | W1n.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1n.footer.primary` | 44vw | 6vh | W1n.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1n.footer.singleAction` | 90vw | 6vh | W1n.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: inventorySnapshot, equipmentContext, selectedItemRef
PARENT: W1; BODY: selection/detail, embeddable inside W1e
plan = ExistingEquipmentPreview(selectedItemRef, equipmentContext)
model.body = InventoryRowsAndSelectedComparison(plan)
model.actions = BackAndEquip(plan.allowed, plan.reason)
RenderInExistingW1BodyOrMountW1AsContextRequires()
ON selectItem: UpdateLocalSelectionAndPreview()
ON Equip: DispatchExistingEquipmentCommandWithStableRef()
RefreshFromDomainReceipt(); never mutate equipment in renderer.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/equipment.js:297](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/equipment.js#L297) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3), [WC2a1](wireframe-gallery.html#WC2a1)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Equipment": [
      {
        "label": "Iron blade",
        "value": "Owned",
        "kind": "choice"
      }
    ],
    "Consumables": [
      {
        "label": "Healing draught",
        "value": "1 available",
        "kind": "choice"
      }
    ]
  },
  "primary": "Select",
  "references": [
    {
      "file": "src/ui/screens/equipment.js",
      "symbol": "mountEquipment",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC2a1"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1o: Item inspection

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Item name                                  [×] │
├────────────────────────────────────────────────┤
│ [Art, if present] │ Type · tags                │
│ Stats / requirements                           │
│ Effects                                        │
├────────────────────────────────────────────────┤
│ [           Applicable item action           ] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1o.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1o.header` | 95vw | 10vh | W1o.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1o.header.title` | 77vw | 6vh | W1o.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1o.header.exit` | 8vw | 6vh | W1o.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1o.body` | 95vw | 70vh | W1o.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1o.body.activePane` | 90vw | 66vh | W1o.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1o.body.art` | 44vw | 66vh | W1o.body.activePane | next column, left to right | center / center | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1o.body.itemFacts` | 44vw | 66vh | W1o.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1o.footer` | 95vw | 10vh | W1o.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1o.footer.closeBack` | 44vw | 6vh | W1o.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1o.footer.primary` | 44vw | 6vh | W1o.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1o.footer.singleAction` | 90vw | 6vh | W1o.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Item name                    [×] │
│ [Small art, if present]          │
│ Type · tags                      │
│ Stats / requirements             │
│ Effects                          │
├──────────────────────────────────┤
│ [    Applicable item action    ] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1o.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1o.header` | 95vw | 10vh | W1o.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1o.header.title` | 77vw | 6vh | W1o.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1o.header.exit` | 8vw | 6vh | W1o.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1o.body` | 95vw | 70vh | W1o.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1o.body.activePane` | 90vw | 66vh | W1o.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1o.body.art` | 90vw | 32vh | W1o.body.activePane | next row, top to bottom | center / center | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1o.body.itemFacts` | 90vw | 32vh | W1o.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1o.footer` | 95vw | 10vh | W1o.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1o.footer.closeBack` | 44vw | 6vh | W1o.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1o.footer.primary` | 44vw | 6vh | W1o.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1o.footer.singleAction` | 90vw | 6vh | W1o.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Item name             [×] │
│ [Art, if present]         │
│                           │
│ Type · tags               │
│ Stats / requirements      │
│ Effects                   │
│                           │
├───────────────────────────┤
│ [Applicable item action ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1o.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1o.header` | 95vw | 10vh | W1o.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1o.header.title` | 77vw | 6vh | W1o.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1o.header.exit` | 8vw | 6vh | W1o.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1o.body` | 95vw | 70vh | W1o.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1o.body.activePane` | 90vw | 66vh | W1o.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1o.body.art` | 90vw | 32vh | W1o.body.activePane | next row, top to bottom | center / center | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1o.body.itemFacts` | 90vw | 32vh | W1o.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1o.footer` | 95vw | 10vh | W1o.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1o.footer.closeBack` | 44vw | 6vh | W1o.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1o.footer.primary` | 44vw | 6vh | W1o.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1o.footer.singleAction` | 90vw | 6vh | W1o.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Item name             [×] │
│ [Art, if present]         │
│                           │
│ Type · tags               │
│ Stats / requirements      │
│ Effects                   │
│                           │
├───────────────────────────┤
│ [Applicable item action ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1o.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1o.header` | 95vw | 10vh | W1o.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1o.header.title` | 77vw | 6vh | W1o.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1o.header.exit` | 8vw | 6vh | W1o.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1o.body` | 95vw | 70vh | W1o.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1o.body.activePane` | 90vw | 66vh | W1o.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1o.body.art` | 90vw | 32vh | W1o.body.activePane | next row, top to bottom | center / center | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1o.body.itemFacts` | 90vw | 32vh | W1o.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1o.footer` | 95vw | 10vh | W1o.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1o.footer.closeBack` | 44vw | 6vh | W1o.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1o.footer.primary` | 44vw | 6vh | W1o.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1o.footer.singleAction` | 90vw | 6vh | W1o.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: itemDefinitionRef, instanceSnapshot, inspectionContext
PARENT: W1; BODY: inspection
model.body = ItemNameArtTagsStatsRequirementsAndEffects()
model.actions = ProjectOnlyApplicableItemActions(inspectionContext)
RenderWithW1(model, categoryRail = absent)
ON action: DispatchBoundDomainIntentWithCurrentInstanceRef()
ON close: RestoreOpenerThroughW0()
Inspection alone is read-only; no invented primary button.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/cardInspection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardInspection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WC2a1](wireframe-gallery.html#WC2a1)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Details": [
      {
        "label": "Effect",
        "value": "Projected from item model",
        "kind": "fact"
      },
      {
        "label": "Requirements",
        "value": "Met",
        "kind": "fact"
      },
      {
        "label": "Rarity",
        "value": "Common",
        "kind": "fact"
      }
    ]
  },
  "primary": null,
  "references": [
    {
      "file": "src/ui/components/cardInspection.js",
      "symbol": "Card inspection",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC2a1"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1p: Combatant inspection

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Combatant name                             [×] │
├────────────────────────────────────────────────┤
│ [Art, if present] │ HP / resources             │
│ Status / intentions                            │
│ Known combat facts                             │
├────────────────────────────────────────────────┤
│ [                   Close                    ] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1p.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1p.header` | 95vw | 10vh | W1p.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1p.header.title` | 77vw | 6vh | W1p.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1p.header.exit` | 8vw | 6vh | W1p.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1p.body` | 95vw | 70vh | W1p.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1p.body.activePane` | 90vw | 66vh | W1p.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1p.body.portrait` | 44vw | 66vh | W1p.body.activePane | next column, left to right | center / center | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1p.body.combatantFacts` | 44vw | 66vh | W1p.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1p.footer` | 95vw | 10vh | W1p.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1p.footer.closeBack` | 44vw | 6vh | W1p.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1p.footer.primary` | 44vw | 6vh | W1p.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1p.footer.singleAction` | 90vw | 6vh | W1p.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Combatant name               [×] │
│ [Small art, if present]          │
│ HP / resources                   │
│ Status / intentions              │
│ Known combat facts               │
├──────────────────────────────────┤
│ [            Close             ] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1p.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1p.header` | 95vw | 10vh | W1p.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1p.header.title` | 77vw | 6vh | W1p.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1p.header.exit` | 8vw | 6vh | W1p.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1p.body` | 95vw | 70vh | W1p.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1p.body.activePane` | 90vw | 66vh | W1p.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1p.body.portrait` | 90vw | 32vh | W1p.body.activePane | next row, top to bottom | center / center | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1p.body.combatantFacts` | 90vw | 32vh | W1p.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1p.footer` | 95vw | 10vh | W1p.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1p.footer.closeBack` | 44vw | 6vh | W1p.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1p.footer.primary` | 44vw | 6vh | W1p.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1p.footer.singleAction` | 90vw | 6vh | W1p.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Combatant name        [×] │
│ [Art, if present]         │
│                           │
│ HP / resources            │
│ Status / intentions       │
│ Known combat facts        │
│                           │
├───────────────────────────┤
│ [         Close         ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1p.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1p.header` | 95vw | 10vh | W1p.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1p.header.title` | 77vw | 6vh | W1p.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1p.header.exit` | 8vw | 6vh | W1p.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1p.body` | 95vw | 70vh | W1p.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1p.body.activePane` | 90vw | 66vh | W1p.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1p.body.portrait` | 90vw | 32vh | W1p.body.activePane | next row, top to bottom | center / center | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1p.body.combatantFacts` | 90vw | 32vh | W1p.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1p.footer` | 95vw | 10vh | W1p.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1p.footer.closeBack` | 44vw | 6vh | W1p.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1p.footer.primary` | 44vw | 6vh | W1p.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1p.footer.singleAction` | 90vw | 6vh | W1p.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Combatant name        [×] │
│ [Art, if present]         │
│                           │
│ HP / resources            │
│ Status / intentions       │
│ Known combat facts        │
│                           │
├───────────────────────────┤
│ [         Close         ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1p.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1p.header` | 95vw | 10vh | W1p.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1p.header.title` | 77vw | 6vh | W1p.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1p.header.exit` | 8vw | 6vh | W1p.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1p.body` | 95vw | 70vh | W1p.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1p.body.activePane` | 90vw | 66vh | W1p.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1p.body.portrait` | 90vw | 32vh | W1p.body.activePane | next row, top to bottom | center / center | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1p.body.combatantFacts` | 90vw | 32vh | W1p.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1p.footer` | 95vw | 10vh | W1p.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1p.footer.closeBack` | 44vw | 6vh | W1p.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1p.footer.primary` | 44vw | 6vh | W1p.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1p.footer.singleAction` | 90vw | 6vh | W1p.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: combatantRef, committedCombatSnapshot, visibilityRules
PARENT: W1; BODY: inspection
model.body = VisibleCombatantIdentityVitalsStatusesAndIntent()
model.actions = CloseIfApplicable()
RenderWithW1(model, categoryRail = absent)
ON snapshotUpdate: RefreshVisibleFactsWithoutStealingFocus()
ON close: RestoreCombatInputScopeThroughW0()
Do not reveal hidden intent or mutate combat during inspection.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/combatantInspector.js:148](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantInspector.js#L148) — Current owner checkout (including inspected local edits)
- [src/ui/models/CombatantInspectorModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/CombatantInspectorModel.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WC4b](wireframe-gallery.html#WC4b), [WCF4](wireframe-gallery.html#WCF4), [WCM1](wireframe-gallery.html#WCM1), [WCI2](wireframe-gallery.html#WCI2)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {},
  "primary": null,
  "references": [
    {
      "file": "src/ui/components/combatantInspector.js",
      "symbol": "Combatant inspector",
      "status": "Current source reference; proposed W0/W1 layout"
    },
    {
      "file": "src/ui/models/CombatantInspectorModel.js",
      "symbol": "Combatant inspector model",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC4b",
  "inspector": true
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1q: Potion inspection

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Potion name                                [×] │
├────────────────────────────────────────────────┤
│ [Art, if present] │ Charges / capacity         │
│ Effect                                         │
│ Use eligibility                                │
├────────────────────────────────────────────────┤
│ [                Use if legal                ] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1q.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1q.header` | 95vw | 10vh | W1q.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1q.header.title` | 77vw | 6vh | W1q.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1q.header.exit` | 8vw | 6vh | W1q.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1q.body` | 95vw | 70vh | W1q.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1q.body.activePane` | 90vw | 66vh | W1q.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1q.body.art` | 44vw | 66vh | W1q.body.activePane | next column, left to right | center / center | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1q.body.potionFacts` | 44vw | 66vh | W1q.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1q.footer` | 95vw | 10vh | W1q.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1q.footer.closeBack` | 44vw | 6vh | W1q.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1q.footer.primary` | 44vw | 6vh | W1q.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1q.footer.singleAction` | 90vw | 6vh | W1q.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Potion name                  [×] │
│ [Small art, if present]          │
│ Charges / capacity               │
│ Effect                           │
│ Use eligibility                  │
├──────────────────────────────────┤
│ [         Use if legal         ] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1q.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1q.header` | 95vw | 10vh | W1q.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1q.header.title` | 77vw | 6vh | W1q.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1q.header.exit` | 8vw | 6vh | W1q.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1q.body` | 95vw | 70vh | W1q.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1q.body.activePane` | 90vw | 66vh | W1q.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1q.body.art` | 90vw | 32vh | W1q.body.activePane | next row, top to bottom | center / center | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1q.body.potionFacts` | 90vw | 32vh | W1q.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1q.footer` | 95vw | 10vh | W1q.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1q.footer.closeBack` | 44vw | 6vh | W1q.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1q.footer.primary` | 44vw | 6vh | W1q.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1q.footer.singleAction` | 90vw | 6vh | W1q.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Potion name           [×] │
│ [Art, if present]         │
│                           │
│ Charges / capacity        │
│ Effect                    │
│ Use eligibility           │
│                           │
├───────────────────────────┤
│ [     Use if legal      ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1q.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1q.header` | 95vw | 10vh | W1q.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1q.header.title` | 77vw | 6vh | W1q.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1q.header.exit` | 8vw | 6vh | W1q.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1q.body` | 95vw | 70vh | W1q.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1q.body.activePane` | 90vw | 66vh | W1q.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1q.body.art` | 90vw | 32vh | W1q.body.activePane | next row, top to bottom | center / center | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1q.body.potionFacts` | 90vw | 32vh | W1q.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1q.footer` | 95vw | 10vh | W1q.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1q.footer.closeBack` | 44vw | 6vh | W1q.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1q.footer.primary` | 44vw | 6vh | W1q.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1q.footer.singleAction` | 90vw | 6vh | W1q.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Potion name           [×] │
│ [Art, if present]         │
│                           │
│ Charges / capacity        │
│ Effect                    │
│ Use eligibility           │
│                           │
├───────────────────────────┤
│ [     Use if legal      ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1q.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1q.header` | 95vw | 10vh | W1q.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1q.header.title` | 77vw | 6vh | W1q.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1q.header.exit` | 8vw | 6vh | W1q.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1q.body` | 95vw | 70vh | W1q.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1q.body.activePane` | 90vw | 66vh | W1q.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1q.body.art` | 90vw | 32vh | W1q.body.activePane | next row, top to bottom | center / center | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1q.body.potionFacts` | 90vw | 32vh | W1q.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1q.footer` | 95vw | 10vh | W1q.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1q.footer.closeBack` | 44vw | 6vh | W1q.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1q.footer.primary` | 44vw | 6vh | W1q.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1q.footer.singleAction` | 90vw | 6vh | W1q.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: potionRef, runSnapshot, useContext
PARENT: W1; BODY: inspection
plan = ExistingPotionUsePlan(potionRef, useContext)
model.body = PotionIdentityChargesEffectAndEligibility(plan)
model.actions = UseOnlyWhenOfferedByContext()
RenderWithW1(model, categoryRail = absent)
ON Use: DispatchExistingPotionCommandAndRevalidateCharges()
ON result: RefreshChargesAndEffectReceipt()
Charges retain semantic resource colors; Use follows primary-state palette.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/flask.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/flask.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3), [WC2c1](wireframe-gallery.html#WC2c1)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Details": [
      {
        "label": "Effect",
        "value": "Restore health up to maximum",
        "kind": "fact"
      },
      {
        "label": "Charges",
        "value": "1 / 1",
        "kind": "fact"
      },
      {
        "label": "Availability",
        "value": "Requires missing health",
        "kind": "fact"
      }
    ]
  },
  "primary": "Use",
  "references": [
    {
      "file": "src/ui/components/flask.js",
      "symbol": "Flask inspection",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC2c1"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1r: Save status

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. No art slot. Retry saves storage; it never repeats a gameplay transaction.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Save game                                  [×] │
│                                                │
│ Character · Location                           │
│ Destination: active slot                       │
│ Last saved: …                                  │
│ [Saved status / error]                         │
├────────────────────────────────────────────────┤
│ [Back]                                  [Save] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1r.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1r.header` | 95vw | 10vh | W1r.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1r.header.title` | 77vw | 6vh | W1r.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1r.header.exit` | 8vw | 6vh | W1r.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1r.body` | 95vw | 70vh | W1r.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1r.body.activePane` | 90vw | 66vh | W1r.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1r.body.saveIdentity` | 44vw | 66vh | W1r.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1r.body.saveStatus` | 44vw | 66vh | W1r.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1r.footer` | 95vw | 10vh | W1r.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1r.footer.closeBack` | 44vw | 6vh | W1r.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1r.footer.primary` | 44vw | 6vh | W1r.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1r.footer.singleAction` | 90vw | 6vh | W1r.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Save game                    [×] │
│ Character · Location             │
│ Destination: active slot         │
│ Last saved: …                    │
│ [Saved status / error]           │
├──────────────────────────────────┤
│ [Back]                    [Save] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1r.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1r.header` | 95vw | 10vh | W1r.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1r.header.title` | 77vw | 6vh | W1r.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1r.header.exit` | 8vw | 6vh | W1r.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1r.body` | 95vw | 70vh | W1r.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1r.body.activePane` | 90vw | 66vh | W1r.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1r.body.saveIdentity` | 90vw | 32vh | W1r.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1r.body.saveStatus` | 90vw | 32vh | W1r.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1r.footer` | 95vw | 10vh | W1r.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1r.footer.closeBack` | 44vw | 6vh | W1r.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1r.footer.primary` | 44vw | 6vh | W1r.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1r.footer.singleAction` | 90vw | 6vh | W1r.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Save game             [×] │
│                           │
│ Character · Location      │
│ Destination: active slot  │
│ Last saved: …             │
│ [Saved status / error]    │
│                           │
├───────────────────────────┤
│ [Back]             [Save] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1r.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1r.header` | 95vw | 10vh | W1r.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1r.header.title` | 77vw | 6vh | W1r.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1r.header.exit` | 8vw | 6vh | W1r.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1r.body` | 95vw | 70vh | W1r.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1r.body.activePane` | 90vw | 66vh | W1r.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1r.body.saveIdentity` | 90vw | 32vh | W1r.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1r.body.saveStatus` | 90vw | 32vh | W1r.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1r.footer` | 95vw | 10vh | W1r.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1r.footer.closeBack` | 44vw | 6vh | W1r.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1r.footer.primary` | 44vw | 6vh | W1r.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1r.footer.singleAction` | 90vw | 6vh | W1r.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Save game             [×] │
│                           │
│ Character · Location      │
│ Destination: active slot  │
│ Last saved: …             │
│ [Saved status / error]    │
│                           │
├───────────────────────────┤
│ [Back]             [Save] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1r.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1r.header` | 95vw | 10vh | W1r.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1r.header.title` | 77vw | 6vh | W1r.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1r.header.exit` | 8vw | 6vh | W1r.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1r.body` | 95vw | 70vh | W1r.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1r.body.activePane` | 90vw | 66vh | W1r.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1r.body.saveIdentity` | 90vw | 32vh | W1r.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1r.body.saveStatus` | 90vw | 32vh | W1r.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1r.footer` | 95vw | 10vh | W1r.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1r.footer.closeBack` | 44vw | 6vh | W1r.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1r.footer.primary` | 44vw | 6vh | W1r.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1r.footer.singleAction` | 90vw | 6vh | W1r.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: activeRunIdentity, activeSlotIdentity, saveStatus
PARENT: W1; BODY: status, no art slot
model.body = CharacterLocationDestinationLastSavedAndStatus()
model.actions = BackAndSave(existingSaveCapability)
RenderWithW1(model)
ON Save:
    SetBusy(); result = PersistenceCoordinator.SaveCurrentCommittedRun()
    ClearBusy(); DisplaySavedOrFailure(result)
ON Retry: RetryPersistenceOnly()
Do not replay gameplay commands or silently select another destination.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/saveSlotSelector.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/saveSlotSelector.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Status": [
      {
        "label": "Save state",
        "value": "Saved",
        "kind": "fact"
      },
      {
        "label": "Slot",
        "value": "Slot 1",
        "kind": "fact"
      },
      {
        "label": "Location",
        "value": "Act 1 · Floor 3",
        "kind": "fact"
      }
    ]
  },
  "primary": null,
  "references": [
    {
      "file": "src/ui/components/saveSlotSelector.js",
      "symbol": "Save slot model projection",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1s: Rest

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Rest                       {Status}            │
│                                                │
│ [Recover] [Service]                            │
│ Recovery values / service cost                 │
│ Availability / reason                          │
│                                                │
├────────────────────────────────────────────────┤
│ [                  Continue                  ] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1s.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1s.header` | 95vw | 10vh | W1s.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1s.header.title` | 77vw | 6vh | W1s.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1s.header.exit` | 8vw | 6vh | W1s.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1s.body` | 95vw | 70vh | W1s.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1s.body.activePane` | 90vw | 66vh | W1s.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1s.body.restChoices` | 44vw | 66vh | W1s.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1s.body.consequences` | 44vw | 66vh | W1s.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1s.footer` | 95vw | 10vh | W1s.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1s.footer.closeBack` | 44vw | 6vh | W1s.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1s.footer.primary` | 44vw | 6vh | W1s.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1s.footer.singleAction` | 90vw | 6vh | W1s.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Rest · {Status}                  │
│ [Recover] [Service]              │
│ Recovery values / service cost   │
│ Availability / reason            │
├──────────────────────────────────┤
│ [           Continue           ] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1s.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1s.header` | 95vw | 10vh | W1s.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1s.header.title` | 77vw | 6vh | W1s.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1s.header.exit` | 8vw | 6vh | W1s.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1s.body` | 95vw | 70vh | W1s.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1s.body.activePane` | 90vw | 66vh | W1s.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1s.body.restChoices` | 90vw | 32vh | W1s.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1s.body.consequences` | 90vw | 32vh | W1s.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1s.footer` | 95vw | 10vh | W1s.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1s.footer.closeBack` | 44vw | 6vh | W1s.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1s.footer.primary` | 44vw | 6vh | W1s.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1s.footer.singleAction` | 90vw | 6vh | W1s.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Rest                      │
│ {Status}                  │
│                           │
│ [Recover] [Service]       │
│ Recovery values / service │
│ cost                      │
│ Availability / reason     │
│                           │
├───────────────────────────┤
│ [       Continue        ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1s.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1s.header` | 95vw | 10vh | W1s.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1s.header.title` | 77vw | 6vh | W1s.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1s.header.exit` | 8vw | 6vh | W1s.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1s.body` | 95vw | 70vh | W1s.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1s.body.activePane` | 90vw | 66vh | W1s.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1s.body.restChoices` | 90vw | 32vh | W1s.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1s.body.consequences` | 90vw | 32vh | W1s.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1s.footer` | 95vw | 10vh | W1s.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1s.footer.closeBack` | 44vw | 6vh | W1s.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1s.footer.primary` | 44vw | 6vh | W1s.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1s.footer.singleAction` | 90vw | 6vh | W1s.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Rest                      │
│ {Status}                  │
│                           │
│ [Recover] [Service]       │
│ Recovery values / service │
│ cost                      │
│ Availability / reason     │
│                           │
├───────────────────────────┤
│ [       Continue        ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1s.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1s.header` | 95vw | 10vh | W1s.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1s.header.title` | 77vw | 6vh | W1s.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1s.header.exit` | 8vw | 6vh | W1s.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1s.body` | 95vw | 70vh | W1s.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1s.body.activePane` | 90vw | 66vh | W1s.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1s.body.restChoices` | 90vw | 32vh | W1s.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1s.body.consequences` | 90vw | 32vh | W1s.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1s.footer` | 95vw | 10vh | W1s.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1s.footer.closeBack` | 44vw | 6vh | W1s.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1s.footer.primary` | 44vw | 6vh | W1s.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1s.footer.singleAction` | 90vw | 6vh | W1s.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: restDefinition, runSnapshot, refillAndServiceContext
PARENT: W1; BODY: choice/progression
plan = ExistingRestAndServicePlans()
model.body = RecoveryValuesOptionsCostsAndReasons(plan)
model.actions = ExistingContinueOrRestAction()
RenderWithW1(model)
ON service: OpenRegisteredServicePresenter()
ON recovery: DispatchValidatedRestCommand()
ON result: PreserveExistingOneUseOrMultiUseContinuation()
No healing, refill, or service mutation from rendering a choice.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/rest.js:98](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/rest.js#L98) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Rest": [
      {
        "label": "Health",
        "value": "32 / 40",
        "kind": "fact"
      },
      {
        "label": "Recovery",
        "value": "Preview from rest plan",
        "kind": "fact"
      }
    ],
    "Prepare": [
      {
        "label": "Flasks",
        "value": "Allocate charges",
        "kind": "fact"
      }
    ],
    "Upgrade": [
      {
        "label": "Equipment",
        "value": "Select eligible item",
        "kind": "fact"
      }
    ]
  },
  "primary": "Review rest",
  "references": [
    {
      "file": "src/ui/screens/rest.js",
      "symbol": "mountRest",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1t: Rewards

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Rewards                       {Status}         │
│                                                │
│ [Reward] [Reward]                              │
│ Claimed / available state                      │
│ Required choice, if any                        │
│                                                │
├────────────────────────────────────────────────┤
│ [                  Continue                  ] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1t.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1t.header` | 95vw | 10vh | W1t.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1t.header.title` | 77vw | 6vh | W1t.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1t.header.exit` | 8vw | 6vh | W1t.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1t.body` | 95vw | 70vh | W1t.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1t.body.activePane` | 90vw | 66vh | W1t.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1t.body.rewardChoices` | 44vw | 66vh | W1t.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1t.body.claimStatus` | 44vw | 66vh | W1t.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1t.footer` | 95vw | 10vh | W1t.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1t.footer.closeBack` | 44vw | 6vh | W1t.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1t.footer.primary` | 44vw | 6vh | W1t.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1t.footer.singleAction` | 90vw | 6vh | W1t.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Rewards · {Status}               │
│ [Reward] [Reward]                │
│ Claimed / available state        │
│ Required choice, if any          │
├──────────────────────────────────┤
│ [           Continue           ] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1t.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1t.header` | 95vw | 10vh | W1t.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1t.header.title` | 77vw | 6vh | W1t.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1t.header.exit` | 8vw | 6vh | W1t.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1t.body` | 95vw | 70vh | W1t.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1t.body.activePane` | 90vw | 66vh | W1t.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1t.body.rewardChoices` | 90vw | 32vh | W1t.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1t.body.claimStatus` | 90vw | 32vh | W1t.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1t.footer` | 95vw | 10vh | W1t.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1t.footer.closeBack` | 44vw | 6vh | W1t.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1t.footer.primary` | 44vw | 6vh | W1t.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1t.footer.singleAction` | 90vw | 6vh | W1t.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Rewards                   │
│ {Status}                  │
│                           │
│ [Reward] [Reward]         │
│ Claimed / available state │
│ Required choice, if any   │
│                           │
├───────────────────────────┤
│ [       Continue        ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1t.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1t.header` | 95vw | 10vh | W1t.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1t.header.title` | 77vw | 6vh | W1t.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1t.header.exit` | 8vw | 6vh | W1t.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1t.body` | 95vw | 70vh | W1t.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1t.body.activePane` | 90vw | 66vh | W1t.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1t.body.rewardChoices` | 90vw | 32vh | W1t.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1t.body.claimStatus` | 90vw | 32vh | W1t.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1t.footer` | 95vw | 10vh | W1t.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1t.footer.closeBack` | 44vw | 6vh | W1t.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1t.footer.primary` | 44vw | 6vh | W1t.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1t.footer.singleAction` | 90vw | 6vh | W1t.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Rewards                   │
│ {Status}                  │
│                           │
│ [Reward] [Reward]         │
│ Claimed / available state │
│ Required choice, if any   │
│                           │
├───────────────────────────┤
│ [       Continue        ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1t.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1t.header` | 95vw | 10vh | W1t.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1t.header.title` | 77vw | 6vh | W1t.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1t.header.exit` | 8vw | 6vh | W1t.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1t.body` | 95vw | 70vh | W1t.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1t.body.activePane` | 90vw | 66vh | W1t.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1t.body.rewardChoices` | 90vw | 32vh | W1t.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1t.body.claimStatus` | 90vw | 32vh | W1t.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1t.footer` | 95vw | 10vh | W1t.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1t.footer.closeBack` | 44vw | 6vh | W1t.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1t.footer.primary` | 44vw | 6vh | W1t.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1t.footer.singleAction` | 90vw | 6vh | W1t.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: persistedPendingRewards, currentPossessions, claimState
PARENT: W1; BODY: choice/progression
plan = ExistingRewardPlan(pendingRewards, capacities)
model.body = RewardChoicesAndClaimedAvailableBlockedStates(plan)
model.actions = ContinueFromExistingRewardPolicy(plan)
RenderWithW1(model)
ON chooseReward: DispatchRegisteredClaimCommand(stableRewardRef)
ON Continue: ResolveExistingAutomaticAndOptionalClaimsThroughDomainCommand()
ON result: PersistPendingRewardProgressAndNavigateWhenComplete()
Reload/duplicate activation must not grant a reward twice or reroll choices.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/reward.js:70](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/reward.js#L70) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3), [WC1a](wireframe-gallery.html#WC1a)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Cards": [
      {
        "label": "Strike",
        "value": "Choose reward",
        "kind": "choice"
      }
    ],
    "Equipment": [
      {
        "label": "Iron blade",
        "value": "Choose reward",
        "kind": "choice"
      }
    ],
    "Currency": [
      {
        "label": "Cinders",
        "value": "Award from reward plan",
        "kind": "fact"
      }
    ]
  },
  "primary": "Claim",
  "references": [
    {
      "file": "src/ui/screens/reward.js",
      "symbol": "mountRewards",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC1a"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1u: Event

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Preserve story text and legal mandatory choices.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Event name                       {Status}      │
│                                                │
│ Necessary authored narrative                   │
│ [Response] [Response]                          │
│ Cost / consequence                             │
│                                                │
├────────────────────────────────────────────────┤
│ [           Continue when allowed            ] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1u.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1u.header` | 95vw | 10vh | W1u.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1u.header.title` | 77vw | 6vh | W1u.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1u.header.exit` | 8vw | 6vh | W1u.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1u.body` | 95vw | 70vh | W1u.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1u.body.activePane` | 90vw | 66vh | W1u.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1u.body.narrative` | 44vw | 66vh | W1u.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1u.body.responses` | 44vw | 66vh | W1u.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1u.footer` | 95vw | 10vh | W1u.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1u.footer.closeBack` | 44vw | 6vh | W1u.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1u.footer.primary` | 44vw | 6vh | W1u.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1u.footer.singleAction` | 90vw | 6vh | W1u.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Event name · {Status}            │
│ Necessary authored narrative     │
│ [Response] [Response]            │
│ Cost / consequence               │
├──────────────────────────────────┤
│ [    Continue when allowed     ] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1u.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1u.header` | 95vw | 10vh | W1u.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1u.header.title` | 77vw | 6vh | W1u.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1u.header.exit` | 8vw | 6vh | W1u.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1u.body` | 95vw | 70vh | W1u.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1u.body.activePane` | 90vw | 66vh | W1u.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1u.body.narrative` | 90vw | 32vh | W1u.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1u.body.responses` | 90vw | 32vh | W1u.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1u.footer` | 95vw | 10vh | W1u.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1u.footer.closeBack` | 44vw | 6vh | W1u.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1u.footer.primary` | 44vw | 6vh | W1u.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1u.footer.singleAction` | 90vw | 6vh | W1u.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Event name                │
│ {Status}                  │
│                           │
│ Necessary authored        │
│ narrative                 │
│ [Response] [Response]     │
│ Cost / consequence        │
│                           │
├───────────────────────────┤
│ [ Continue when allowed ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1u.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1u.header` | 95vw | 10vh | W1u.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1u.header.title` | 77vw | 6vh | W1u.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1u.header.exit` | 8vw | 6vh | W1u.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1u.body` | 95vw | 70vh | W1u.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1u.body.activePane` | 90vw | 66vh | W1u.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1u.body.narrative` | 90vw | 32vh | W1u.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1u.body.responses` | 90vw | 32vh | W1u.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1u.footer` | 95vw | 10vh | W1u.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1u.footer.closeBack` | 44vw | 6vh | W1u.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1u.footer.primary` | 44vw | 6vh | W1u.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1u.footer.singleAction` | 90vw | 6vh | W1u.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Event name                │
│ {Status}                  │
│                           │
│ Necessary authored        │
│ narrative                 │
│ [Response] [Response]     │
│ Cost / consequence        │
│                           │
├───────────────────────────┤
│ [ Continue when allowed ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1u.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1u.header` | 95vw | 10vh | W1u.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1u.header.title` | 77vw | 6vh | W1u.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1u.header.exit` | 8vw | 6vh | W1u.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1u.body` | 95vw | 70vh | W1u.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1u.body.activePane` | 90vw | 66vh | W1u.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1u.body.narrative` | 90vw | 32vh | W1u.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1u.body.responses` | 90vw | 32vh | W1u.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1u.footer` | 95vw | 10vh | W1u.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1u.footer.closeBack` | 44vw | 6vh | W1u.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1u.footer.primary` | 44vw | 6vh | W1u.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1u.footer.singleAction` | 90vw | 6vh | W1u.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: eventDefinition, questHistory, runSnapshot
PARENT: W1; BODY: choice/progression, page presentation when required
choices = ExistingAvailableEventChoices(history, snapshot)
model.body = EventTitleNarrativeChoicesAndConsequences(choices)
model.actions = OnlyLegalExistingContinuationActions()
model.capabilities.close = ApprovedEventExitCapability()
RenderWithW1(model)
ON chooseResponse(choiceId):
    ApplyExistingConfirmationPolicy()
    DispatchEventCommandRevalidatingRequirementsAndRecordingHistoryOnce()
ON result: ShowResultOrNavigateToExistingCombatTransition()
Rendering and navigation Back never apply event effects.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/event.js:8](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L8) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Encounter": [
      {
        "label": "A sheltered crossroads",
        "value": "A figure waits beside the ruined gate.",
        "kind": "fact"
      },
      {
        "label": "Speak",
        "value": "Discover what they know",
        "kind": "choice"
      },
      {
        "label": "Leave",
        "value": "Return to the road",
        "kind": "choice"
      }
    ]
  },
  "primary": "Continue",
  "references": [
    {
      "file": "src/ui/screens/event.js",
      "symbol": "mountEvent",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1v: Shop offers

**Parent: W1.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Body only inside W1d; category name need not be repeated when navigation labels it clearly.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Selected shop category                         │
│ {Status}                                       │
│                                                │
│ [Offer + price] [Offer + price]                │
│ Availability with each offer                   │
│ Selected offer detail                          │
│                                                │
├────────────────────────────────────────────────┤
│ [           Selected offer action            ] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1v.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1v.header` | 95vw | 10vh | W1v.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1v.header.title` | 77vw | 6vh | W1v.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1v.header.exit` | 8vw | 6vh | W1v.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1v.body` | 95vw | 70vh | W1v.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1v.body.activePane` | 90vw | 66vh | W1v.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1v.body.offers` | 44vw | 66vh | W1v.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1v.body.offerDetail` | 44vw | 66vh | W1v.body.activePane | next column, left to right | start / start | normal grid flow | 2vw column gap | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1v.footer` | 95vw | 10vh | W1v.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1v.footer.closeBack` | 44vw | 6vh | W1v.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1v.footer.primary` | 44vw | 6vh | W1v.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1v.footer.singleAction` | 90vw | 6vh | W1v.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Selected shop category ·         │
│ {Status}                         │
│ [Offer + price] [Offer + price]  │
│ Availability with each offer     │
│ Selected offer detail            │
├──────────────────────────────────┤
│ [    Selected offer action     ] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1v.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1v.header` | 95vw | 10vh | W1v.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1v.header.title` | 77vw | 6vh | W1v.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1v.header.exit` | 8vw | 6vh | W1v.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1v.body` | 95vw | 70vh | W1v.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1v.body.activePane` | 90vw | 66vh | W1v.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1v.body.offers` | 90vw | 32vh | W1v.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1v.body.offerDetail` | 90vw | 32vh | W1v.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1v.footer` | 95vw | 10vh | W1v.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1v.footer.closeBack` | 44vw | 6vh | W1v.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1v.footer.primary` | 44vw | 6vh | W1v.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1v.footer.singleAction` | 90vw | 6vh | W1v.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Selected shop category    │
│ {Status}                  │
│                           │
│ [Offer + price] [Offer +  │
│ price]                    │
│ Availability with each    │
│ offer                     │
│ Selected offer detail     │
│                           │
├───────────────────────────┤
│ [ Selected offer action ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1v.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1v.header` | 95vw | 10vh | W1v.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1v.header.title` | 77vw | 6vh | W1v.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1v.header.exit` | 8vw | 6vh | W1v.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1v.body` | 95vw | 70vh | W1v.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1v.body.activePane` | 90vw | 66vh | W1v.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1v.body.offers` | 90vw | 32vh | W1v.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1v.body.offerDetail` | 90vw | 32vh | W1v.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1v.footer` | 95vw | 10vh | W1v.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1v.footer.closeBack` | 44vw | 6vh | W1v.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1v.footer.primary` | 44vw | 6vh | W1v.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1v.footer.singleAction` | 90vw | 6vh | W1v.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Selected shop category    │
│ {Status}                  │
│                           │
│ [Offer + price] [Offer +  │
│ price]                    │
│ Availability with each    │
│ offer                     │
│ Selected offer detail     │
│                           │
├───────────────────────────┤
│ [ Selected offer action ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1v.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1v.header` | 95vw | 10vh | W1v.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1v.header.title` | 77vw | 6vh | W1v.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1v.header.exit` | 8vw | 6vh | W1v.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1v.body` | 95vw | 70vh | W1v.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1v.body.activePane` | 90vw | 66vh | W1v.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1v.body.offers` | 90vw | 32vh | W1v.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1v.body.offerDetail` | 90vw | 32vh | W1v.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W1v.footer` | 95vw | 10vh | W1v.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1v.footer.closeBack` | 44vw | 6vh | W1v.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1v.footer.primary` | 44vw | 6vh | W1v.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1v.footer.singleAction` | 90vw | 6vh | W1v.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: activeShopCategory, savedStock, runSnapshot
PARENT: W1; BODY embedded in W1d, not another complete shell
offers = DomainShopQuery(activeCategory, snapshot, savedStock)
body = OfferCardsWithPricesEligibilityAndSelectedDetails(offers)
ReturnBodyModelToW1d(body)
ON offerSelected: EmitSelectionIntent(stableOfferRef)
ON offerActivated: EmitRegisteredBuySellOrRemoveIntent()
W1d owns navigation/footer; domain command owns pricing and mutation.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/shop.js:65](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/shop.js#L65) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3), [WC2a1](wireframe-gallery.html#WC2a1)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {
    "Offers": [
      {
        "label": "Iron blade",
        "value": "40 Cinders",
        "kind": "choice"
      },
      {
        "label": "Healing draught",
        "value": "20 Cinders",
        "kind": "choice"
      }
    ]
  },
  "primary": "Review offer",
  "references": [
    {
      "file": "src/ui/screens/shop.js",
      "symbol": "mountShop",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC2a1"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W1w: Entity inspector

**Parent: W1.** Inherits W1/W0. Combatant context: entity name is the modal title. Left is sprite, name and HP only; no intent/defense/effect overlays. Right starts with HP/Intent/Defense in one row, followed by Current state (stance, resources, statuses, buildup in matching label/value rows), Previous actions newest-first, Known abilities, Known traits (weaknesses/resistances), then Lore. Use shared left-aligned label/value columns throughout. Only right details scroll; header/footer stay anchored. No placeholder action: Back fills footer width. Actual contextual commands may use inherited inline two-action footer. Other entities retain their registered detail model. Source facts come from one knowledge-filtered snapshot; examples are illustrative.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ {Combatant name}                           [×] │
├────────────────────────────────────────────────┤
│ Sprite      │ HP | Intent | Defense            │
│             │ Current state                    │
│             │ Stance / resources / effects     │
│             │ Previous actions                 │
│             │ Known abilities                  │
│             │ Known traits                     │
│ Name        │ Lore                             │
│ [HP bar]    │ Details scroll independently     │
├────────────────────────────────────────────────┤
│ [                    Back                    ] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1w.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1w.header` | 95vw | 10vh | W1w.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1w.header.title` | 77vw | 6vh | W1w.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1w.header.exit` | 8vw | 6vh | W1w.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1w.body` | 95vw | 70vh | W1w.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1w.body.activePane` | 90vw | 66vh | W1w.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W1w.body.cardPreview` | 30.6vw | 66vh | W1w.body.activePane | left / top | start / start | two-column grid in every mode | 2vw column gap; body inset inherited | Left: contained sprite/name/HP; max14rem preview width; no overlays |
| `W1w.body.details` | 57.4vw | 66vh | W1w.body.activePane | right / top | start / start | two-column grid in every mode | 2vw column gap; body inset inherited | Right scroll owner; uniform label/value columns |
| `W1w.body.details.summary` | 57.4vw | 6vh | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Top: HP / Intent / Defense in one horizontal row |
| `W1w.body.details.currentState` | 57.4vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.previousActions` | 57.4vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.knownAbilities` | 57.4vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.knownTraits` | 57.4vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.lore` | 57.4vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.footer` | 95vw | 10vh | W1w.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1w.footer.closeBack` | 44vw | 6vh | W1w.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1w.footer.primary` | 44vw | 6vh | W1w.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1w.footer.singleAction` | 90vw | 6vh | W1w.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ {Name}                       [×] │
├──────────────────────────────────┤
│ Sprite  │ HP / Intent / Defense  │
│         │ Current state          │
│         │ Previous actions       │
│         │ Known abilities        │
│         │ Known traits           │
│ Name    │ Lore                   │
│ [HP]    │ Scroll details         │
├──────────────────────────────────┤
│ [             Back             ] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1w.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1w.header` | 95vw | 10vh | W1w.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1w.header.title` | 77vw | 6vh | W1w.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1w.header.exit` | 8vw | 6vh | W1w.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1w.body` | 95vw | 70vh | W1w.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1w.body.activePane` | 90vw | 66vh | W1w.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1w.body.cardPreview` | 30.6vw | 66vh | W1w.body.activePane | left / top | start / start | two-column grid in every mode | 2vw column gap; body inset inherited | Left: contained sprite/name/HP; max14rem preview width; no overlays |
| `W1w.body.details` | 57.4vw | 66vh | W1w.body.activePane | right / top | start / start | two-column grid in every mode | 2vw column gap; body inset inherited | Right scroll owner; uniform label/value columns |
| `W1w.body.details.summary` | 57.4vw | 6vh | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Top: HP / Intent / Defense in one horizontal row |
| `W1w.body.details.currentState` | 57.4vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.previousActions` | 57.4vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.knownAbilities` | 57.4vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.knownTraits` | 57.4vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.lore` | 57.4vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.footer` | 95vw | 10vh | W1w.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1w.footer.closeBack` | 44vw | 6vh | W1w.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1w.footer.primary` | 44vw | 6vh | W1w.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1w.footer.singleAction` | 90vw | 6vh | W1w.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ {Name}                [×] │
├───────────────────────────┤
│ Sprite │ HP Intent        │
│ Defense                   │
│        │ Current state    │
│        │ Previous actions │
│        │ Known abilities  │
│        │ Known traits     │
│ Name   │ Lore             │
│ [HP]   │ Details scroll   │
├───────────────────────────┤
│ [         Back          ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1w.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1w.header` | 95vw | 10vh | W1w.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1w.header.title` | 77vw | 6vh | W1w.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1w.header.exit` | 8vw | 6vh | W1w.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1w.body` | 95vw | 70vh | W1w.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1w.body.activePane` | 90vw | 66vh | W1w.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1w.body.cardPreview` | 28.8vw | 66vh | W1w.body.activePane | left / top | start / start | two-column grid in every mode | 2vw column gap; body inset inherited | Left: contained sprite/name/HP; max14rem preview width; no overlays |
| `W1w.body.details` | 59.2vw | 66vh | W1w.body.activePane | right / top | start / start | two-column grid in every mode | 2vw column gap; body inset inherited | Right scroll owner; uniform label/value columns |
| `W1w.body.details.summary` | 59.2vw | 6vh | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Top: HP / Intent / Defense in one horizontal row |
| `W1w.body.details.currentState` | 59.2vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.previousActions` | 59.2vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.knownAbilities` | 59.2vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.knownTraits` | 59.2vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.lore` | 59.2vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.footer` | 95vw | 10vh | W1w.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1w.footer.closeBack` | 44vw | 6vh | W1w.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1w.footer.primary` | 44vw | 6vh | W1w.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1w.footer.singleAction` | 90vw | 6vh | W1w.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ {Name}                [×] │
├───────────────────────────┤
│ Sprite │ HP Intent        │
│ Defense                   │
│        │ Current state    │
│        │ Previous actions │
│        │ Known abilities  │
│        │ Known traits     │
│ Name   │ Lore             │
│ [HP]   │ Details scroll   │
├───────────────────────────┤
│ [         Back          ] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W1w.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W1w.header` | 95vw | 10vh | W1w.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W1w.header.title` | 77vw | 6vh | W1w.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W1w.header.exit` | 8vw | 6vh | W1w.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W1w.body` | 95vw | 70vh | W1w.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W1w.body.activePane` | 90vw | 66vh | W1w.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W1w.body.cardPreview` | 28.8vw | 66vh | W1w.body.activePane | left / top | start / start | two-column grid in every mode | 2vw column gap; body inset inherited | Left: contained sprite/name/HP; max14rem preview width; no overlays |
| `W1w.body.details` | 59.2vw | 66vh | W1w.body.activePane | right / top | start / start | two-column grid in every mode | 2vw column gap; body inset inherited | Right scroll owner; uniform label/value columns |
| `W1w.body.details.summary` | 59.2vw | 6vh | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Top: HP / Intent / Defense in one horizontal row |
| `W1w.body.details.currentState` | 59.2vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.previousActions` | 59.2vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.knownAbilities` | 59.2vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.knownTraits` | 59.2vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.body.details.lore` | 59.2vw | auto; content | W1w.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Ordered normal-flow section; content height may exceed scroll viewport |
| `W1w.footer` | 95vw | 10vh | W1w.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W1w.footer.closeBack` | 44vw | 6vh | W1w.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W1w.footer.primary` | 44vw | 6vh | W1w.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W1w.footer.singleAction` | 90vw | 6vh | W1w.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, context, returnFocusTarget
PARENT: W1
ResolveCurrentEntityOrShowUnavailableState()
MountInheritedW1Shell(title, exit, inlineFooter)
ReplaceCategoryRailWithReadOnlyCardPreview()
UseTwoColumns(leftCard=config.inspector.previewFractionForMode, gap=config.inspector.gapVw, rightDetails=remaining)
ContainLeftPreview(maxWidth=config.inspector.maxPreviewWidthRem); reserve header and footer
AlignFactLabelsAndValuesToSharedLeftAlignedColumns()
IfNoRelevantDomainAction: RenderFullWidthBackButton()
RenderWC0PreviewWithInheritedRatiosAndNoNestedInfoButton()
IF combatant: RenderOnlySpriteNameAndHPInLeftPreview()
IF combatant: ProjectAvailableResourcesIntentDefenseStanceStatusesBuildupIntoRightPane()
UseRegisteredDetailProvidersFromTheSameCombatantSnapshot()
SetModalTitle(combatant.displayName)
RenderTopSummary(HP, intent, defense)
FilterCurrentOptionalFactsByDomainActivity(); preserve known history/lore
RenderOrderedSections(currentState, previousActions, knownAbilities, knownTraits, lore)
ApplyPlayerKnowledgeFilterBeforeRendering(); distinguish unknown from none
AlignAllFactLabelsAndValuesToSharedColumns(); body scrolls independently
ProjectDetailsFromRegisteredTagComponentsAndDomainQueries()
KeepCardLeftAndDetailsRightInWideCompactPortrait()
ScrollOnlyDetailsWhenRequired; preserve readable minimums
ON primary: ExistingDomainCommandRevalidatesEntityAndContext()
ON close/back/escape: DisposeThenRestoreOriginSelectionAndFocus()
ON entityRemoved: DisableDomainActionsAndShowUnavailableReason()
No timer callbacks or preview activation may open another inspector.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/combatantInspector.js:148](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantInspector.js#L148) — Current owner checkout (including inspected local edits)
- [src/ui/models/CombatantInspectorModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/CombatantInspectorModel.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WC4b](wireframe-gallery.html#WC4b), [WCF4](wireframe-gallery.html#WCF4), [WCM1](wireframe-gallery.html#WCM1), [WCI2](wireframe-gallery.html#WCI2)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "categories": {},
  "primary": null,
  "references": [
    {
      "file": "src/ui/components/combatantInspector.js",
      "symbol": "Combatant inspector",
      "status": "Current source reference; proposed W0/W1 layout"
    },
    {
      "file": "src/ui/models/CombatantInspectorModel.js",
      "symbol": "Knowledge-filtered facts",
      "status": "Current source reference; proposed W0/W1 layout"
    }
  ],
  "card": "WC4b",
  "inspector": true
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe W2: Confirmation variant

**Parent: W0.** Compact decision shell sharing W1 primitives/lifecycle. Safe focus, cancellation, shielding, and emphasis are centrally implemented policies.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ {Concrete question}                        [×] │
│                                                │
│ {Target identity}                              │
│ {Exact consequence}                            │
│                                                │
│ [Back]                                [Action] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2.frame` | 50vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2.header` | 50vw | 10vh | W2.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2.header.title` | 32vw | 6vh | W2.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2.header.exit` | 8vw | 6vh | W2.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2.body` | 50vw | 30vh | W2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2.body.activePane` | 45vw | 26vh | W2.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W2.body.target` | 45vw | 12vh | W2.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2.body.consequence` | 45vw | 12vh | W2.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2.footer` | 50vw | 10vh | W2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2.footer.closeBack` | 21.5vw | 6vh | W2.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2.footer.primary` | 21.5vw | 6vh | W2.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2.footer.singleAction` | 45vw | 6vh | W2.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ {Concrete question}          [×] │
│ {Target identity}                │
│ {Exact consequence}              │
│ [Back]                  [Action] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2.header` | 90vw | 10vh | W2.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2.header.title` | 72vw | 6vh | W2.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2.header.exit` | 8vw | 6vh | W2.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2.body` | 90vw | 30vh | W2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2.body.activePane` | 85vw | 26vh | W2.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2.body.target` | 85vw | 12vh | W2.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2.body.consequence` | 85vw | 12vh | W2.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2.footer` | 90vw | 10vh | W2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2.footer.closeBack` | 41.5vw | 6vh | W2.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2.footer.primary` | 41.5vw | 6vh | W2.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2.footer.singleAction` | 85vw | 6vh | W2.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ {Concrete question}   [×] │
│                           │
│ {Target identity}         │
│ {Exact consequence}       │
│                           │
│ [Back]           [Action] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2.header` | 90vw | 10vh | W2.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2.header.title` | 72vw | 6vh | W2.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2.header.exit` | 8vw | 6vh | W2.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2.body` | 90vw | 30vh | W2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2.body.activePane` | 85vw | 26vh | W2.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2.body.target` | 85vw | 12vh | W2.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2.body.consequence` | 85vw | 12vh | W2.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2.footer` | 90vw | 10vh | W2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2.footer.closeBack` | 41.5vw | 6vh | W2.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2.footer.primary` | 41.5vw | 6vh | W2.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2.footer.singleAction` | 85vw | 6vh | W2.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ {Concrete question}   [×] │
│                           │
│ {Target identity}         │
│ {Exact consequence}       │
│                           │
│ [Back]           [Action] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2.header` | 90vw | 10vh | W2.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2.header.title` | 72vw | 6vh | W2.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2.header.exit` | 8vw | 6vh | W2.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2.body` | 90vw | 30vh | W2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2.body.activePane` | 85vw | 26vh | W2.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2.body.target` | 85vw | 12vh | W2.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2.body.consequence` | 85vw | 12vh | W2.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2.footer` | 90vw | 10vh | W2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2.footer.closeBack` | 41.5vw | 6vh | W2.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2.footer.primary` | 41.5vw | 6vh | W2.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2.footer.singleAction` | 85vw | 6vh | W2.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: exactTarget, consequence, decisionPolicy, confirmBinding
PARENT: W0; VARIANT: compact confirmation
model.body = ConcreteQuestionTargetAndConsequence()
model.actions = BackAndConfirmWithCurrentEligibility()
model.paletteException = DestructivePolicyIfApplicable()
MountThroughW0(model); FocusSafeActionAccordingToPolicy()
ON confirm:
    IF alreadyCommitting: RETURN
    RevalidateExactTargetAndReviewedConsequence()
    IF changed: RefreshReviewWithoutCommitting(); RETURN
    result = DispatchBoundCommandOnce(stableRequestId)
    IF rejected: KeepDecisionOpenAndShowReason()
    ELSE CloseWithConfirmReasonAndPreserveNavigationInputShield()
ON Back/Exit: CancelOnceWithoutCallingConfirm()
ON replace/dispose: ReleaseResourcesWithoutPretendingUserConfirmedOrCancelled()
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "prompt": "Review the selected action.",
  "primary": "Confirm"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W2a: Service confirmation

**Parent: W2.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Apply this service?                        [×] │
│                                                │
│ Item / target                                  │
│ Exact cost and changes                         │
│                                                │
│ [Back]                               [Confirm] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2a.frame` | 50vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2a.header` | 50vw | 10vh | W2a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2a.header.title` | 32vw | 6vh | W2a.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2a.header.exit` | 8vw | 6vh | W2a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2a.body` | 50vw | 30vh | W2a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2a.body.activePane` | 45vw | 26vh | W2a.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W2a.body.target` | 45vw | 12vh | W2a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2a.body.consequence` | 45vw | 12vh | W2a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2a.footer` | 50vw | 10vh | W2a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2a.footer.closeBack` | 21.5vw | 6vh | W2a.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2a.footer.primary` | 21.5vw | 6vh | W2a.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2a.footer.singleAction` | 45vw | 6vh | W2a.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Apply this service?          [×] │
│ Item / target                    │
│ Exact cost and changes           │
│ [Back]                 [Confirm] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2a.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2a.header` | 90vw | 10vh | W2a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2a.header.title` | 72vw | 6vh | W2a.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2a.header.exit` | 8vw | 6vh | W2a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2a.body` | 90vw | 30vh | W2a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2a.body.activePane` | 85vw | 26vh | W2a.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2a.body.target` | 85vw | 12vh | W2a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2a.body.consequence` | 85vw | 12vh | W2a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2a.footer` | 90vw | 10vh | W2a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2a.footer.closeBack` | 41.5vw | 6vh | W2a.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2a.footer.primary` | 41.5vw | 6vh | W2a.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2a.footer.singleAction` | 85vw | 6vh | W2a.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Apply this service?   [×] │
│                           │
│ Item / target             │
│ Exact cost and changes    │
│                           │
│ [Back]          [Confirm] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2a.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2a.header` | 90vw | 10vh | W2a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2a.header.title` | 72vw | 6vh | W2a.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2a.header.exit` | 8vw | 6vh | W2a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2a.body` | 90vw | 30vh | W2a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2a.body.activePane` | 85vw | 26vh | W2a.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2a.body.target` | 85vw | 12vh | W2a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2a.body.consequence` | 85vw | 12vh | W2a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2a.footer` | 90vw | 10vh | W2a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2a.footer.closeBack` | 41.5vw | 6vh | W2a.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2a.footer.primary` | 41.5vw | 6vh | W2a.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2a.footer.singleAction` | 85vw | 6vh | W2a.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Apply this service?   [×] │
│                           │
│ Item / target             │
│ Exact cost and changes    │
│                           │
│ [Back]          [Confirm] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2a.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2a.header` | 90vw | 10vh | W2a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2a.header.title` | 72vw | 6vh | W2a.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2a.header.exit` | 8vw | 6vh | W2a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2a.body` | 90vw | 30vh | W2a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2a.body.activePane` | 85vw | 26vh | W2a.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2a.body.target` | 85vw | 12vh | W2a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2a.body.consequence` | 85vw | 12vh | W2a.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2a.footer` | 90vw | 10vh | W2a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2a.footer.closeBack` | 41.5vw | 6vh | W2a.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2a.footer.primary` | 41.5vw | 6vh | W2a.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2a.footer.singleAction` | 85vw | 6vh | W2a.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: selectedServicePlan, targetRef, existingConfirmationPolicy
PARENT: W2
model = ServiceQuestionExactCostChangesAndTarget(selectedServicePlan)
confirmBinding = RegisteredServiceCommand(targetRef, reviewedConsequence)
RenderWithW2(model, confirmBinding)
ON stalePlan: RefreshSelectionReviewInsteadOfChargingDifferentCost()
ON success: ReturnReceiptToServicePresenter()
All arming, focus, cancellation and input shielding remain inherited.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "prompt": "Apply the selected service at the quoted cost?",
  "primary": "Confirm service"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W2b: Delete save

**Parent: W2.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Delete this save?                          [×] │
│                                                │
│ Slot · Character                               │
│ Actual loss / retention policy                 │
│                                                │
│ [Back]                                [Delete] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2b.frame` | 50vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2b.header` | 50vw | 10vh | W2b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2b.header.title` | 32vw | 6vh | W2b.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2b.header.exit` | 8vw | 6vh | W2b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2b.body` | 50vw | 30vh | W2b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2b.body.activePane` | 45vw | 26vh | W2b.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W2b.body.target` | 45vw | 12vh | W2b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2b.body.consequence` | 45vw | 12vh | W2b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2b.footer` | 50vw | 10vh | W2b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2b.footer.closeBack` | 21.5vw | 6vh | W2b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2b.footer.primary` | 21.5vw | 6vh | W2b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2b.footer.singleAction` | 45vw | 6vh | W2b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Delete this save?            [×] │
│ Slot · Character                 │
│ Actual loss / retention policy   │
│ [Back]                  [Delete] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2b.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2b.header` | 90vw | 10vh | W2b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2b.header.title` | 72vw | 6vh | W2b.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2b.header.exit` | 8vw | 6vh | W2b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2b.body` | 90vw | 30vh | W2b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2b.body.activePane` | 85vw | 26vh | W2b.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2b.body.target` | 85vw | 12vh | W2b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2b.body.consequence` | 85vw | 12vh | W2b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2b.footer` | 90vw | 10vh | W2b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2b.footer.closeBack` | 41.5vw | 6vh | W2b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2b.footer.primary` | 41.5vw | 6vh | W2b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2b.footer.singleAction` | 85vw | 6vh | W2b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Delete this save?     [×] │
│                           │
│ Slot · Character          │
│ Actual loss / retention   │
│ policy                    │
│                           │
│ [Back]           [Delete] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2b.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2b.header` | 90vw | 10vh | W2b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2b.header.title` | 72vw | 6vh | W2b.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2b.header.exit` | 8vw | 6vh | W2b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2b.body` | 90vw | 30vh | W2b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2b.body.activePane` | 85vw | 26vh | W2b.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2b.body.target` | 85vw | 12vh | W2b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2b.body.consequence` | 85vw | 12vh | W2b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2b.footer` | 90vw | 10vh | W2b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2b.footer.closeBack` | 41.5vw | 6vh | W2b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2b.footer.primary` | 41.5vw | 6vh | W2b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2b.footer.singleAction` | 85vw | 6vh | W2b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Delete this save?     [×] │
│                           │
│ Slot · Character          │
│ Actual loss / retention   │
│ policy                    │
│                           │
│ [Back]           [Delete] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2b.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2b.header` | 90vw | 10vh | W2b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2b.header.title` | 72vw | 6vh | W2b.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2b.header.exit` | 8vw | 6vh | W2b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2b.body` | 90vw | 30vh | W2b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2b.body.activePane` | 85vw | 26vh | W2b.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2b.body.target` | 85vw | 12vh | W2b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2b.body.consequence` | 85vw | 12vh | W2b.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2b.footer` | 90vw | 10vh | W2b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2b.footer.closeBack` | 41.5vw | 6vh | W2b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2b.footer.primary` | 41.5vw | 6vh | W2b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2b.footer.singleAction` | 85vw | 6vh | W2b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: selectedSaveIdentity, actualRetentionPolicy
PARENT: W2
model = DeleteQuestionForExactSlotAndCharacter()
model.consequence = DeriveActualDeletionOrArchiveConsequence()
model.paletteException = destructive
confirmBinding = DeleteSaveIfIdentityStillMatches()
RenderWithW2(model, confirmBinding)
ON targetChanged: RequireUpdatedReview(); do not delete replacement occupant
ON success: RefreshSlotList(); preserve profile data
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "prompt": "Delete Slot 1? This removes the saved climb.",
  "primary": "Delete save",
  "danger": true
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W2c: Replace save

**Parent: W2.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Replace this save?                         [×] │
│                                                │
│ Slot · Existing character                      │
│ Replacement: new character                     │
│ Exact replacement consequence                  │
│                                                │
│ [Back]                               [Replace] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2c.frame` | 50vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2c.header` | 50vw | 10vh | W2c.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2c.header.title` | 32vw | 6vh | W2c.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2c.header.exit` | 8vw | 6vh | W2c.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2c.body` | 50vw | 30vh | W2c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2c.body.activePane` | 45vw | 26vh | W2c.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W2c.body.target` | 45vw | 12vh | W2c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2c.body.consequence` | 45vw | 12vh | W2c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2c.footer` | 50vw | 10vh | W2c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2c.footer.closeBack` | 21.5vw | 6vh | W2c.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2c.footer.primary` | 21.5vw | 6vh | W2c.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2c.footer.singleAction` | 45vw | 6vh | W2c.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Replace this save?           [×] │
│ Slot · Existing character        │
│ Replacement: new character       │
│ Exact replacement consequence    │
│ [Back]                 [Replace] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2c.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2c.header` | 90vw | 10vh | W2c.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2c.header.title` | 72vw | 6vh | W2c.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2c.header.exit` | 8vw | 6vh | W2c.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2c.body` | 90vw | 30vh | W2c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2c.body.activePane` | 85vw | 26vh | W2c.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2c.body.target` | 85vw | 12vh | W2c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2c.body.consequence` | 85vw | 12vh | W2c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2c.footer` | 90vw | 10vh | W2c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2c.footer.closeBack` | 41.5vw | 6vh | W2c.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2c.footer.primary` | 41.5vw | 6vh | W2c.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2c.footer.singleAction` | 85vw | 6vh | W2c.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Replace this save?    [×] │
│                           │
│ Slot · Existing character │
│ Replacement: new          │
│ character                 │
│ Exact replacement         │
│ consequence               │
│                           │
│ [Back]          [Replace] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2c.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2c.header` | 90vw | 10vh | W2c.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2c.header.title` | 72vw | 6vh | W2c.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2c.header.exit` | 8vw | 6vh | W2c.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2c.body` | 90vw | 30vh | W2c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2c.body.activePane` | 85vw | 26vh | W2c.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2c.body.target` | 85vw | 12vh | W2c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2c.body.consequence` | 85vw | 12vh | W2c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2c.footer` | 90vw | 10vh | W2c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2c.footer.closeBack` | 41.5vw | 6vh | W2c.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2c.footer.primary` | 41.5vw | 6vh | W2c.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2c.footer.singleAction` | 85vw | 6vh | W2c.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Replace this save?    [×] │
│                           │
│ Slot · Existing character │
│ Replacement: new          │
│ character                 │
│ Exact replacement         │
│ consequence               │
│                           │
│ [Back]          [Replace] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2c.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2c.header` | 90vw | 10vh | W2c.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2c.header.title` | 72vw | 6vh | W2c.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2c.header.exit` | 8vw | 6vh | W2c.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2c.body` | 90vw | 30vh | W2c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2c.body.activePane` | 85vw | 26vh | W2c.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2c.body.target` | 85vw | 12vh | W2c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2c.body.consequence` | 85vw | 12vh | W2c.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2c.footer` | 90vw | 10vh | W2c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2c.footer.closeBack` | 41.5vw | 6vh | W2c.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2c.footer.primary` | 41.5vw | 6vh | W2c.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2c.footer.singleAction` | 85vw | 6vh | W2c.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: occupiedSlotIdentity, validatedNewRunDraft
PARENT: W2
model = ReplacementQuestionOldIdentityNewIdentityAndConsequence()
model.paletteException = destructive
confirmBinding = CommitNewRunToReviewedDestinationWithExistingSaveSemantics()
RenderWithW2(model, confirmBinding)
ON cancel: KeepDraftAndExistingSave()
ON failure: ReportActualStorageOutcome(); never claim replacement succeeded
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "prompt": "Replace the saved climb in Slot 1?",
  "primary": "Replace save",
  "danger": true
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W2d: Load over active run

**Parent: W2.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Load this save?                            [×] │
│                                                │
│ Destination slot · Character                   │
│ Current progress consequence                   │
│                                                │
│ [Back]                                  [Load] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2d.frame` | 50vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2d.header` | 50vw | 10vh | W2d.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2d.header.title` | 32vw | 6vh | W2d.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2d.header.exit` | 8vw | 6vh | W2d.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2d.body` | 50vw | 30vh | W2d.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2d.body.activePane` | 45vw | 26vh | W2d.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W2d.body.target` | 45vw | 12vh | W2d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2d.body.consequence` | 45vw | 12vh | W2d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2d.footer` | 50vw | 10vh | W2d.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2d.footer.closeBack` | 21.5vw | 6vh | W2d.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2d.footer.primary` | 21.5vw | 6vh | W2d.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2d.footer.singleAction` | 45vw | 6vh | W2d.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Load this save?              [×] │
│ Destination slot · Character     │
│ Current progress consequence     │
│ [Back]                    [Load] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2d.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2d.header` | 90vw | 10vh | W2d.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2d.header.title` | 72vw | 6vh | W2d.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2d.header.exit` | 8vw | 6vh | W2d.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2d.body` | 90vw | 30vh | W2d.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2d.body.activePane` | 85vw | 26vh | W2d.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2d.body.target` | 85vw | 12vh | W2d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2d.body.consequence` | 85vw | 12vh | W2d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2d.footer` | 90vw | 10vh | W2d.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2d.footer.closeBack` | 41.5vw | 6vh | W2d.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2d.footer.primary` | 41.5vw | 6vh | W2d.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2d.footer.singleAction` | 85vw | 6vh | W2d.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Load this save?       [×] │
│                           │
│ Destination slot ·        │
│ Character                 │
│ Current progress          │
│ consequence               │
│                           │
│ [Back]             [Load] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2d.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2d.header` | 90vw | 10vh | W2d.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2d.header.title` | 72vw | 6vh | W2d.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2d.header.exit` | 8vw | 6vh | W2d.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2d.body` | 90vw | 30vh | W2d.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2d.body.activePane` | 85vw | 26vh | W2d.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2d.body.target` | 85vw | 12vh | W2d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2d.body.consequence` | 85vw | 12vh | W2d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2d.footer` | 90vw | 10vh | W2d.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2d.footer.closeBack` | 41.5vw | 6vh | W2d.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2d.footer.primary` | 41.5vw | 6vh | W2d.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2d.footer.singleAction` | 85vw | 6vh | W2d.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Load this save?       [×] │
│                           │
│ Destination slot ·        │
│ Character                 │
│ Current progress          │
│ consequence               │
│                           │
│ [Back]             [Load] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2d.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2d.header` | 90vw | 10vh | W2d.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2d.header.title` | 72vw | 6vh | W2d.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2d.header.exit` | 8vw | 6vh | W2d.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2d.body` | 90vw | 30vh | W2d.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2d.body.activePane` | 85vw | 26vh | W2d.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2d.body.target` | 85vw | 12vh | W2d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2d.body.consequence` | 85vw | 12vh | W2d.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2d.footer` | 90vw | 10vh | W2d.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2d.footer.closeBack` | 41.5vw | 6vh | W2d.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2d.footer.primary` | 41.5vw | 6vh | W2d.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2d.footer.singleAction` | 85vw | 6vh | W2d.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: selectedLoadIdentity, currentRunSaveStatus
PARENT: W2
model = LoadQuestionWithExactCurrentProgressConsequence()
confirmBinding = ExistingLoadCommandForReviewedIdentity()
RenderWithW2(model, confirmBinding)
ON cancel: ReturnToActiveRunWithoutMutation()
ON success: DisposeOldPresenterAndRestoreLoadedRunThroughNavigationCoordinator()
Do not add an unrequested autosave or silently replace the load target.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "prompt": "Load Slot 1 and leave the active climb?",
  "primary": "Load save"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W2e: Quit confirmation

**Parent: W2.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Labels reflect the actual selected quit operation.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Leave this run?                            [×] │
│                                                │
│ Current run identity                           │
│ Exact save / loss consequence                  │
│                                                │
│ [Back]                              [Continue] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2e.frame` | 50vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2e.header` | 50vw | 10vh | W2e.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2e.header.title` | 32vw | 6vh | W2e.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2e.header.exit` | 8vw | 6vh | W2e.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2e.body` | 50vw | 30vh | W2e.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2e.body.activePane` | 45vw | 26vh | W2e.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W2e.body.target` | 45vw | 12vh | W2e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2e.body.consequence` | 45vw | 12vh | W2e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2e.footer` | 50vw | 10vh | W2e.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2e.footer.closeBack` | 21.5vw | 6vh | W2e.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2e.footer.primary` | 21.5vw | 6vh | W2e.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2e.footer.singleAction` | 45vw | 6vh | W2e.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Leave this run?              [×] │
│ Current run identity             │
│ Exact save / loss consequence    │
│ [Back]                [Continue] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2e.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2e.header` | 90vw | 10vh | W2e.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2e.header.title` | 72vw | 6vh | W2e.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2e.header.exit` | 8vw | 6vh | W2e.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2e.body` | 90vw | 30vh | W2e.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2e.body.activePane` | 85vw | 26vh | W2e.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2e.body.target` | 85vw | 12vh | W2e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2e.body.consequence` | 85vw | 12vh | W2e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2e.footer` | 90vw | 10vh | W2e.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2e.footer.closeBack` | 41.5vw | 6vh | W2e.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2e.footer.primary` | 41.5vw | 6vh | W2e.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2e.footer.singleAction` | 85vw | 6vh | W2e.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Leave this run?       [×] │
│                           │
│ Current run identity      │
│ Exact save / loss         │
│ consequence               │
│                           │
│ [Back]         [Continue] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2e.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2e.header` | 90vw | 10vh | W2e.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2e.header.title` | 72vw | 6vh | W2e.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2e.header.exit` | 8vw | 6vh | W2e.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2e.body` | 90vw | 30vh | W2e.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2e.body.activePane` | 85vw | 26vh | W2e.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2e.body.target` | 85vw | 12vh | W2e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2e.body.consequence` | 85vw | 12vh | W2e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2e.footer` | 90vw | 10vh | W2e.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2e.footer.closeBack` | 41.5vw | 6vh | W2e.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2e.footer.primary` | 41.5vw | 6vh | W2e.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2e.footer.singleAction` | 85vw | 6vh | W2e.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Leave this run?       [×] │
│                           │
│ Current run identity      │
│ Exact save / loss         │
│ consequence               │
│                           │
│ [Back]         [Continue] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W2e.frame` | 90vw | 50vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W2e.header` | 90vw | 10vh | W2e.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W2e.header.title` | 72vw | 6vh | W2e.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W2e.header.exit` | 8vw | 6vh | W2e.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W2e.body` | 90vw | 30vh | W2e.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W2e.body.activePane` | 85vw | 26vh | W2e.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W2e.body.target` | 85vw | 12vh | W2e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2e.body.consequence` | 85vw | 12vh | W2e.body.activePane | next row, top to bottom | start / start | normal grid flow | 2vh row gap | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W2e.footer` | 90vw | 10vh | W2e.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W2e.footer.closeBack` | 41.5vw | 6vh | W2e.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W2e.footer.primary` | 41.5vw | 6vh | W2e.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W2e.footer.singleAction` | 85vw | 6vh | W2e.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: requestedQuitOperation, runIdentity, saveCapabilities
PARENT: W2
model = ConcreteQuitQuestionFromActualSaveOrLossPolicy()
model.primaryLabel = VerbForRequestedOperation()
confirmBinding = ExistingSaveAndQuitOrQuitWithoutSaveCommand()
RenderWithW2(model, confirmBinding)
IF save required AND save fails: RemainAndShowFailure()
ON cancel: RestorePreviousViewThroughW0()
Red destructive exception follows consequences, not the word Quit alone.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5), [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "prompt": "Leave the game? Check the displayed save status.",
  "primary": "Quit"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe W3: Main-menu composition

**Parent: W0.** One title/menu renderer. Title remains screen-centered and Profile top-right. Menu/preview state comes from one view model; keep focus and avoid hover oscillation.

**Wide**

```text
┌────────────────────────────────────────────────┐
│                                  [Profile] [×] │
│                    {Title}                     │
│                                                │
│ {Centered menu OR menu + preview}              │
│                                                │
│                 {Build stamp}                  │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W3.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W3.header` | 95vw | 10vh | W3.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W3.header.title` | 77vw | 6vh | W3.header | screen top-center | center / center | normal grid flow | center on viewport, not leftover header space | Screen-centered override |
| `W3.header.exit` | 8vw | 6vh | W3.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W3.body` | 95vw | 70vh | W3.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W3.body.activePane` | 90vw | 66vh | W3.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W3.body.menu` | 44vw | 66vh | W3.body.activePane | left-center | center / center | normal grid flow | 2vw between menu and preview | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3.body.preview` | 44vw | 66vh | W3.body.activePane | right-center | center / center | normal grid flow | 2vw between menu and preview | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3.footer` | 95vw | 10vh | W3.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W3.footer.closeBack` | 44vw | 6vh | W3.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W3.footer.primary` | 44vw | 6vh | W3.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W3.footer.singleAction` | 90vw | 6vh | W3.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│                    [Profile] [×] │
│             {Title}              │
│ {Menu}                           │
│ {Conditional preview below}      │
│          {Build stamp}           │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W3.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W3.header` | 95vw | 10vh | W3.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W3.header.title` | 77vw | 6vh | W3.header | screen top-center | center / center | normal grid flow | center on viewport, not leftover header space | Screen-centered override |
| `W3.header.exit` | 8vw | 6vh | W3.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W3.body` | 95vw | 70vh | W3.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W3.body.activePane` | 90vw | 66vh | W3.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W3.body.menu` | 90vw | 32vh | W3.body.activePane | top-center | center / center | normal grid flow | 2vh between menu and preview | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3.body.preview` | 90vw | 32vh | W3.body.activePane | below menu | center / center | normal grid flow | 2vh between menu and preview | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3.footer` | 95vw | 10vh | W3.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W3.footer.closeBack` | 44vw | 6vh | W3.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W3.footer.primary` | 44vw | 6vh | W3.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W3.footer.singleAction` | 90vw | 6vh | W3.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│             [Profile] [×] │
│          {Title}          │
│                           │
│ {Menu}                    │
│                           │
│ {Conditional preview}     │
│                           │
│       {Build stamp}       │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W3.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W3.header` | 95vw | 10vh | W3.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W3.header.title` | 77vw | 6vh | W3.header | screen top-center | center / center | normal grid flow | center on viewport, not leftover header space | Screen-centered override |
| `W3.header.exit` | 8vw | 6vh | W3.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W3.body` | 95vw | 70vh | W3.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W3.body.activePane` | 90vw | 66vh | W3.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W3.body.menu` | 90vw | 32vh | W3.body.activePane | top-center | center / center | normal grid flow | 2vh between menu and preview | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3.body.preview` | 90vw | 32vh | W3.body.activePane | below menu | center / center | normal grid flow | 2vh between menu and preview | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3.footer` | 95vw | 10vh | W3.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W3.footer.closeBack` | 44vw | 6vh | W3.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W3.footer.primary` | 44vw | 6vh | W3.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W3.footer.singleAction` | 90vw | 6vh | W3.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│             [Profile] [×] │
│          {Title}          │
│                           │
│ {Menu}                    │
│                           │
│ {Conditional preview}     │
│                           │
│       {Build stamp}       │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W3.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W3.header` | 95vw | 10vh | W3.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W3.header.title` | 77vw | 6vh | W3.header | screen top-center | center / center | normal grid flow | center on viewport, not leftover header space | Screen-centered override |
| `W3.header.exit` | 8vw | 6vh | W3.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W3.body` | 95vw | 70vh | W3.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W3.body.activePane` | 90vw | 66vh | W3.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W3.body.menu` | 90vw | 32vh | W3.body.activePane | top-center | center / center | normal grid flow | 2vh between menu and preview | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3.body.preview` | 90vw | 32vh | W3.body.activePane | below menu | center / center | normal grid flow | 2vh between menu and preview | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3.footer` | 95vw | 10vh | W3.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W3.footer.closeBack` | 44vw | 6vh | W3.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W3.footer.primary` | 44vw | 6vh | W3.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W3.footer.singleAction` | 90vw | 6vh | W3.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: menuDefinition, supportedDestinations, saveSummaries, highlightState
PARENT: W0; VARIANT: full-screen title menu
model.titlePlacement = screenCenter
model.headerExtras = ProfileActionAtTopRight()
canPreview = ContinueIsAvailable() AND highlightedActionId == continue
model.bodyLayout = canPreview ? continuePreview : centeredMenu
model.preview = canPreview ? PreviewOfExactContinueSlot() : absent
IF mode == wide AND canPreview: PlaceMenuLeftAndPreviewRight()
ELSE: CenterMenu(); PlacePreviewBelowOnlyWhenPresent()
RenderWithW0(model); KeepTitleFixedAtFullScreenCenter()
ON hover/focus/controllerHighlight: UpdateOneSharedHighlightState()
ON layoutChange: PreserveNodeIdentityAndPreventPointerHoverOscillation()
ON activate: NavigateThroughRegisteredDestinationBinding()
Highlighting never loads, deletes, or saves a run.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/title.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/title.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "context": "W3"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W3a: Main menu — centered, no preview

**Parent: W3.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Continue is not highlighted. No empty preview placeholder. Existing supported destinations are verified before exposure.

**Wide**

```text
┌────────────────────────────────────────────────┐
│                                  [Profile] [×] │
│                  ASHEN SPIRE                   │
│                                                │
│                    Continue                    │
│                    New game                    │
│                   Load game                    │
│                  Multiplayer                   │
│                    Settings                    │
│                      Quit                      │
│                                                │
│                 [Build stamp]                  │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W3a.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W3a.header` | 95vw | 10vh | W3a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W3a.header.title` | 77vw | 6vh | W3a.header | screen top-center | center / center | normal grid flow | center on viewport, not leftover header space | Screen-centered override |
| `W3a.header.exit` | 8vw | 6vh | W3a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W3a.body` | 95vw | 70vh | W3a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W3a.body.activePane` | 90vw | 66vh | W3a.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W3a.body.menu` | 90vw | 66vh | W3a.body.activePane | center | center / center | normal grid flow | equal free space on both sides | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3a.footer` | 95vw | 10vh | W3a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W3a.footer.closeBack` | 44vw | 6vh | W3a.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W3a.footer.primary` | 44vw | 6vh | W3a.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W3a.footer.singleAction` | 90vw | 6vh | W3a.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│                    [Profile] [×] │
│           ASHEN SPIRE            │
│                                  │
│             Continue             │
│             New game             │
│            Load game             │
│           Multiplayer            │
│             Settings             │
│               Quit               │
│                                  │
│          [Build stamp]           │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W3a.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W3a.header` | 95vw | 10vh | W3a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W3a.header.title` | 77vw | 6vh | W3a.header | screen top-center | center / center | normal grid flow | center on viewport, not leftover header space | Screen-centered override |
| `W3a.header.exit` | 8vw | 6vh | W3a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W3a.body` | 95vw | 70vh | W3a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W3a.body.activePane` | 90vw | 66vh | W3a.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W3a.body.menu` | 90vw | 66vh | W3a.body.activePane | center | center / center | normal grid flow | equal free space on both sides | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3a.footer` | 95vw | 10vh | W3a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W3a.footer.closeBack` | 44vw | 6vh | W3a.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W3a.footer.primary` | 44vw | 6vh | W3a.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W3a.footer.singleAction` | 90vw | 6vh | W3a.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│             [Profile] [×] │
│        ASHEN SPIRE        │
│                           │
│         Continue          │
│         New game          │
│         Load game         │
│        Multiplayer        │
│         Settings          │
│           Quit            │
│                           │
│       [Build stamp]       │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W3a.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W3a.header` | 95vw | 10vh | W3a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W3a.header.title` | 77vw | 6vh | W3a.header | screen top-center | center / center | normal grid flow | center on viewport, not leftover header space | Screen-centered override |
| `W3a.header.exit` | 8vw | 6vh | W3a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W3a.body` | 95vw | 70vh | W3a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W3a.body.activePane` | 90vw | 66vh | W3a.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W3a.body.menu` | 90vw | 66vh | W3a.body.activePane | center | center / center | normal grid flow | equal free space on both sides | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3a.footer` | 95vw | 10vh | W3a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W3a.footer.closeBack` | 44vw | 6vh | W3a.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W3a.footer.primary` | 44vw | 6vh | W3a.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W3a.footer.singleAction` | 90vw | 6vh | W3a.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│             [Profile] [×] │
│        ASHEN SPIRE        │
│                           │
│         Continue          │
│         New game          │
│         Load game         │
│        Multiplayer        │
│         Settings          │
│           Quit            │
│                           │
│       [Build stamp]       │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W3a.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W3a.header` | 95vw | 10vh | W3a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W3a.header.title` | 77vw | 6vh | W3a.header | screen top-center | center / center | normal grid flow | center on viewport, not leftover header space | Screen-centered override |
| `W3a.header.exit` | 8vw | 6vh | W3a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W3a.body` | 95vw | 70vh | W3a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W3a.body.activePane` | 90vw | 66vh | W3a.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W3a.body.menu` | 90vw | 66vh | W3a.body.activePane | center | center / center | normal grid flow | equal free space on both sides | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3a.footer` | 95vw | 10vh | W3a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W3a.footer.closeBack` | 44vw | 6vh | W3a.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W3a.footer.primary` | 44vw | 6vh | W3a.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W3a.footer.singleAction` | 90vw | 6vh | W3a.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: mainMenuContext where Continue is not actively highlighted
PARENT: W3
model.layout = centeredMenu
model.preview = absent
RenderWithW3(model)
DoNotReserveAnEmptyPreviewColumn()
ON availableContinueHighlighted: ReprojectSameInstanceAsW3b()
Preserve supported menu items, title center, Profile anchor, and build stamp.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/title.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/title.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "context": "W3a"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W3b: Main menu — Continue highlighted

**Parent: W3.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Available Continue reveals the exact save preview. Only menu/body layout moves; title stays screen-centered. Compact/mobile preview appears below the centered menu.

**Wide**

```text
┌────────────────────────────────────────────────┐
│                                  [Profile] [×] │
│                  ASHEN SPIRE                   │
│                                                │
│ [Continue]       [Save preview]                │
│ New game         [Character / world]           │
│ Load game                                      │
│ Multiplayer                                    │
│ Settings                                       │
│ Quit                                           │
│                                                │
│ [Matching save identity/location]              │
│                                                │
│                 [Build stamp]                  │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W3b.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W3b.header` | 95vw | 10vh | W3b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W3b.header.title` | 77vw | 6vh | W3b.header | screen top-center | center / center | normal grid flow | center on viewport, not leftover header space | Screen-centered override |
| `W3b.header.exit` | 8vw | 6vh | W3b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W3b.body` | 95vw | 70vh | W3b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W3b.body.activePane` | 90vw | 66vh | W3b.body | after category navigation, if present | stretch / start | normal grid flow | 2.5vw after rail; 2vh top inset | All child slots below share this allocation |
| `W3b.body.menu` | 44vw | 66vh | W3b.body.activePane | left-center | center / center | normal grid flow | 2vw between menu and preview | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3b.body.preview` | 44vw | 66vh | W3b.body.activePane | right-center | center / center | normal grid flow | 2vw between menu and preview | Side-by-side nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3b.footer` | 95vw | 10vh | W3b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W3b.footer.closeBack` | 44vw | 6vh | W3b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W3b.footer.primary` | 44vw | 6vh | W3b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W3b.footer.singleAction` | 90vw | 6vh | W3b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│                    [Profile] [×] │
│           ASHEN SPIRE            │
│                                  │
│             Continue             │
│             New game             │
│            Load game             │
│           Multiplayer            │
│             Settings             │
│               Quit               │
│ [Small save preview]             │
│                                  │
│ [Matching save                   │
│ identity/location]               │
│                                  │
│          [Build stamp]           │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W3b.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W3b.header` | 95vw | 10vh | W3b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W3b.header.title` | 77vw | 6vh | W3b.header | screen top-center | center / center | normal grid flow | center on viewport, not leftover header space | Screen-centered override |
| `W3b.header.exit` | 8vw | 6vh | W3b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W3b.body` | 95vw | 70vh | W3b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W3b.body.activePane` | 90vw | 66vh | W3b.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W3b.body.menu` | 90vw | 32vh | W3b.body.activePane | top-center | center / center | normal grid flow | 2vh between menu and preview | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3b.body.preview` | 90vw | 32vh | W3b.body.activePane | below menu | center / center | normal grid flow | 2vh between menu and preview | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3b.footer` | 95vw | 10vh | W3b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W3b.footer.closeBack` | 44vw | 6vh | W3b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W3b.footer.primary` | 44vw | 6vh | W3b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W3b.footer.singleAction` | 90vw | 6vh | W3b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│             [Profile] [×] │
│        ASHEN SPIRE        │
│                           │
│         Continue          │
│         New game          │
│         Load game         │
│        Multiplayer        │
│         Settings          │
│           Quit            │
│ [Saved character / world] │
│                           │
│ [Matching save            │
│ identity/location]        │
│                           │
│       [Build stamp]       │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W3b.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W3b.header` | 95vw | 10vh | W3b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W3b.header.title` | 77vw | 6vh | W3b.header | screen top-center | center / center | normal grid flow | center on viewport, not leftover header space | Screen-centered override |
| `W3b.header.exit` | 8vw | 6vh | W3b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W3b.body` | 95vw | 70vh | W3b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W3b.body.activePane` | 90vw | 66vh | W3b.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W3b.body.menu` | 90vw | 32vh | W3b.body.activePane | top-center | center / center | normal grid flow | 2vh between menu and preview | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3b.body.preview` | 90vw | 32vh | W3b.body.activePane | below menu | center / center | normal grid flow | 2vh between menu and preview | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3b.footer` | 95vw | 10vh | W3b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W3b.footer.closeBack` | 44vw | 6vh | W3b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W3b.footer.primary` | 44vw | 6vh | W3b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W3b.footer.singleAction` | 90vw | 6vh | W3b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│             [Profile] [×] │
│        ASHEN SPIRE        │
│                           │
│         Continue          │
│         New game          │
│         Load game         │
│        Multiplayer        │
│         Settings          │
│           Quit            │
│ [Saved character / world] │
│                           │
│ [Matching save            │
│ identity/location]        │
│                           │
│       [Build stamp]       │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W3b.frame` | 95vw | 90vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W3b.header` | 95vw | 10vh | W3b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W3b.header.title` | 77vw | 6vh | W3b.header | screen top-center | center / center | normal grid flow | center on viewport, not leftover header space | Screen-centered override |
| `W3b.header.exit` | 8vw | 6vh | W3b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W3b.body` | 95vw | 70vh | W3b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W3b.body.activePane` | 90vw | 66vh | W3b.body | after category navigation, if present | stretch / start | normal grid flow | 2vh after selector / top inset | All child slots below share this allocation |
| `W3b.body.menu` | 90vw | 32vh | W3b.body.activePane | top-center | center / center | normal grid flow | 2vh between menu and preview | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3b.body.preview` | 90vw | 32vh | W3b.body.activePane | below menu | center / center | normal grid flow | 2vh between menu and preview | Stacked nominal subdivision; optional slots collapse and siblings reclaim space |
| `W3b.footer` | 95vw | 10vh | W3b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W3b.footer.closeBack` | 44vw | 6vh | W3b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W3b.footer.primary` | 44vw | 6vh | W3b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W3b.footer.singleAction` | 90vw | 6vh | W3b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: mainMenuContext with available Continue highlighted
PARENT: W3
target = ExactSlotUsedByContinueCommand()
model.preview = SavedCharacterWorldAndSummary(target)
model.layout = wide ? menuLeftPreviewRight : centeredMenuPreviewBelow
RenderWithW3(model)
ON highlightMovesAway OR targetUnavailable: ReprojectSameInstanceAsW3a()
ON ContinueActivated: DispatchExistingLoadNavigation(target)
No title movement; no load or storage writes just to show the preview.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/title.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/title.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "context": "W3b"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe W4: Gameplay / encounter regions

**Parent: W0.** Shared region host, spacing, safe areas, visual states and transitions. Child view models specify region proportions and registered bodies. Combat, map, and dialogue have distinct domain intents; never copy a command or effect merely because the container is shared. Every W2 child is a layer stack, listed bottom to top in gameplay-config.json (layerOrder): the painted scene plate (skyline/skybox, then floor) is the bottom of the stack, scene content stands on it, the context band sits above that, and the HUD and footer bands are the top panels. Each layer is switchable on its own (layers), and where one layer runs under another the overlap is part of the specification, not an accident of layout.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ {Shared HUD}                                   │
│                                                │
│ {Primary scene region}                         │
│                                                │
│ {Context / hand / dialogue body}               │
│                                                │
│ {Bottom actions}                               │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4.header` | 100vw | 10vh | W4.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4.header.title` | 82vw | 6vh | W4.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4.header.exit` | 8vw | 6vh | W4.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4.body` | 100vw | 75vh | W4.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4.scene` | 100vw | 40vh | W4.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4.context` | 100vw | 35vh | W4.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4.scene.battlefield` | 95vw | 36vh | W4.scene | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Art and targets fit inside scene |
| `W4.context.hand` | 95vw | 31vh | W4.context | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Playing-card host allocation, not standalone card size |
| `W4.footer` | 100vw | 15vh | W4.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4.footer.closeBack` | 46.5vw | 6vh | W4.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W4.footer.primary` | 46.5vw | 6vh | W4.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W4.footer.singleAction` | 95vw | 6vh | W4.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ {Compact HUD}                    │
│                                  │
│ {Primary scene}                  │
│                                  │
│ {Context body}                   │
│                                  │
│ {Bottom actions}                 │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4.header` | 100vw | 10vh | W4.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4.header.title` | 82vw | 6vh | W4.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4.header.exit` | 8vw | 6vh | W4.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4.body` | 100vw | 75vh | W4.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4.scene` | 100vw | 40vh | W4.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4.context` | 100vw | 35vh | W4.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4.scene.battlefield` | 95vw | 36vh | W4.scene | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Art and targets fit inside scene |
| `W4.context.hand` | 95vw | 31vh | W4.context | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Playing-card host allocation, not standalone card size |
| `W4.footer` | 100vw | 15vh | W4.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4.footer.closeBack` | 46.5vw | 6vh | W4.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W4.footer.primary` | 46.5vw | 6vh | W4.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W4.footer.singleAction` | 95vw | 6vh | W4.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ {Compact HUD}             │
│                           │
│ {Primary scene}           │
│                           │
│                           │
│                           │
│ {Context body}            │
│                           │
│ {Bottom actions}          │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4.header` | 100vw | 10vh | W4.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4.header.title` | 82vw | 6vh | W4.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4.header.exit` | 8vw | 6vh | W4.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4.body` | 100vw | 75vh | W4.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4.scene` | 100vw | 40vh | W4.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4.context` | 100vw | 35vh | W4.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4.scene.battlefield` | 95vw | 36vh | W4.scene | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Art and targets fit inside scene |
| `W4.context.hand` | 95vw | 31vh | W4.context | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Playing-card host allocation, not standalone card size |
| `W4.footer` | 100vw | 15vh | W4.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4.footer.closeBack` | 46.5vw | 6vh | W4.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W4.footer.primary` | 46.5vw | 6vh | W4.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W4.footer.singleAction` | 95vw | 6vh | W4.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ {Compact HUD}             │
│                           │
│ {Primary scene}           │
│                           │
│                           │
│                           │
│ {Context body}            │
│                           │
│ {Bottom actions}          │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4.header` | 100vw | 10vh | W4.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4.header.title` | 82vw | 6vh | W4.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4.header.exit` | 8vw | 6vh | W4.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4.body` | 100vw | 75vh | W4.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4.scene` | 100vw | 40vh | W4.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4.context` | 100vw | 35vh | W4.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4.scene.battlefield` | 95vw | 36vh | W4.scene | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Art and targets fit inside scene |
| `W4.context.hand` | 95vw | 31vh | W4.context | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Playing-card host allocation, not standalone card size |
| `W4.footer` | 100vw | 15vh | W4.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4.footer.closeBack` | 46.5vw | 6vh | W4.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W4.footer.primary` | 46.5vw | 6vh | W4.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W4.footer.singleAction` | 95vw | 6vh | W4.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: committedSnapshot, regionDefinition, localInteractionState
PARENT: W0; VARIANT: gameplay/encounter region host
model.header = SharedHUDProjection(snapshot)
model.body = RegisteredSceneAndContextBodies(regionDefinition)
model.actions = ProjectLegalContextActions(snapshot)
model.layout = ValidatedRegionFractionsAndMinimums()
RenderWithW0(model)
ConvertViewportToLocalCoordinatesThroughSharedAdapter()
ON resize/rotation: ReflowSameSceneAndSelection(); never reset simulation
ON intent: DispatchLocalOrServerAuthoritativeBindingAccordingToSession()
ON navigation: DisposeSceneBindingsAndTransientEffects()
Share chrome/effects, not domain rules, between combat/map/dialogue.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/screens/map.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/map.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/screens/event.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGS1](wireframe-gallery.html#WGS1), [WGS2](wireframe-gallery.html#WGS2), [WGS3](wireframe-gallery.html#WGS3), [WGS4](wireframe-gallery.html#WGS4), [WGS5](wireframe-gallery.html#WGS5), [WGC1](wireframe-gallery.html#WGC1), [WGC2](wireframe-gallery.html#WGC2), [WGC3](wireframe-gallery.html#WGC3), [WGC4](wireframe-gallery.html#WGC4), [WGC5](wireframe-gallery.html#WGC5), [WGC6](wireframe-gallery.html#WGC6), [WGC7](wireframe-gallery.html#WGC7), [WGC8](wireframe-gallery.html#WGC8), [WGC9](wireframe-gallery.html#WGC9), [WGC10](wireframe-gallery.html#WGC10), [WGC11](wireframe-gallery.html#WGC11), [WGS6](wireframe-gallery.html#WGS6), [WGS7](wireframe-gallery.html#WGS7), [WGH1](wireframe-gallery.html#WGH1), [WGH2](wireframe-gallery.html#WGH2), [WGH3](wireframe-gallery.html#WGH3), [WGH4](wireframe-gallery.html#WGH4), [WGH5](wireframe-gallery.html#WGH5), [WGH6](wireframe-gallery.html#WGH6), [WGH7](wireframe-gallery.html#WGH7), [WGH8](wireframe-gallery.html#WGH8)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [component-completion-client.js](component-completion-client.js), [hud-reference-client.js](hud-reference-client.js), [scene-reference-client.js](scene-reference-client.js)

Renderer binding: `gameplayPreview`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [component-completion.css](component-completion.css), [scene-reference.css](scene-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "context": "W4",
  "scene": {
    "parent": "W4",
    "bands": {
      "hud": 10,
      "scene": 55,
      "context": 30,
      "footer": 5
    },
    "layers": {
      "skyline": true,
      "floor": true,
      "hud": true,
      "actors": true,
      "targets": true,
      "context": true,
      "footer": true
    },
    "playerCount": 6,
    "enemyCount": 6,
    "components": [
      "WGS1",
      "WGS2",
      "WGS3",
      "WGS4",
      "WGS5",
      "WGC1",
      "WGC2",
      "WGC3",
      "WGC4",
      "WGC5",
      "WGC6",
      "WGC7",
      "WGC8",
      "WGC9",
      "WGC10",
      "WGC11",
      "WGS6",
      "WGS7",
      "WGH1",
      "WGH2",
      "WGH3",
      "WGH4",
      "WGH5",
      "WGH6",
      "WGH7",
      "WGH8"
    ],
    "background": {
      "floorHeightPercent": 80,
      "skylineColor": "#635139",
      "floorColor": "#382c20"
    },
    "hud": {
      "context": "combat",
      "enabled": true,
      "layoutPreset": "proposed",
      "layers": {
        "header": true,
        "class": true,
        "cinders": true,
        "position": true,
        "vitality": true,
        "health": true,
        "mana": true,
        "stamina": false,
        "armoury": true,
        "menu": true,
        "rail": false,
        "relics": true,
        "potions": true,
        "experience": true,
        "chargeFlasks": false
      },
      "experience": {
        "contexts": [
          "combat"
        ],
        "color": "#398bd1",
        "heightRem": 0.35,
        "animationMs": 650,
        "awardPreview": 15
      },
      "layout": {
        "gapRem": 0.15,
        "insetRem": 0.15,
        "meterHeightRem": 0.8,
        "actionHeightRem": 2.75,
        "radiusRem": 0.25
      },
      "colors": {
        "background": "#211a12",
        "gold": "#d5af68",
        "text": "#eee2ca",
        "health": "#668c46",
        "mana": "#478dbc",
        "stamina": "#bf9949"
      },
      "sample": {
        "className": "Warden",
        "cinders": 120,
        "act": 1,
        "floor": 4,
        "health": {
          "value": 32,
          "maximum": 40
        },
        "mana": {
          "value": 6,
          "maximum": 10
        },
        "stamina": {
          "value": 8,
          "maximum": 12
        },
        "experience": {
          "value": 40,
          "maximum": 100
        },
        "relics": [
          "Ash seal",
          "Ember charm"
        ],
        "potions": [
          "Smoke vial ×1"
        ],
        "chargeFlasks": [
          "HP ×2",
          "MP ×1"
        ]
      }
    },
    "minimumHudHeightRem": 5.5,
    "layerOrder": [
      {
        "id": "skyline",
        "z": 1
      },
      {
        "id": "floor",
        "z": 2
      },
      {
        "id": "actors",
        "z": 3
      },
      {
        "id": "targets",
        "z": 4
      },
      {
        "id": "context",
        "z": 5
      },
      {
        "id": "hud",
        "z": 6
      },
      {
        "id": "footer",
        "z": 6
      }
    ]
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "scene": {
    "parent": "W4",
    "bands": {
      "hud": 10,
      "scene": 55,
      "context": 30,
      "footer": 5
    },
    "layers": {
      "skyline": true,
      "floor": true,
      "hud": true,
      "actors": true,
      "targets": true,
      "context": true,
      "footer": true
    },
    "playerCount": 6,
    "enemyCount": 6,
    "components": [
      "WGS1",
      "WGS2",
      "WGS3",
      "WGS4",
      "WGS5",
      "WGC1",
      "WGC2",
      "WGC3",
      "WGC4",
      "WGC5",
      "WGC6",
      "WGC7",
      "WGC8",
      "WGC9",
      "WGC10",
      "WGC11",
      "WGS6",
      "WGS7",
      "WGH1",
      "WGH2",
      "WGH3",
      "WGH4",
      "WGH5",
      "WGH6",
      "WGH7",
      "WGH8"
    ],
    "background": {
      "floorHeightPercent": 80,
      "skylineColor": "#635139",
      "floorColor": "#382c20"
    },
    "hud": {
      "context": "combat",
      "enabled": true,
      "layoutPreset": "proposed",
      "layers": {
        "header": true,
        "class": true,
        "cinders": true,
        "position": true,
        "vitality": true,
        "health": true,
        "mana": true,
        "stamina": false,
        "armoury": true,
        "menu": true,
        "rail": false,
        "relics": true,
        "potions": true,
        "experience": true,
        "chargeFlasks": false
      },
      "experience": {
        "contexts": [
          "combat"
        ],
        "color": "#398bd1",
        "heightRem": 0.35,
        "animationMs": 650,
        "awardPreview": 15
      },
      "layout": {
        "gapRem": 0.15,
        "insetRem": 0.15,
        "meterHeightRem": 0.8,
        "actionHeightRem": 2.75,
        "radiusRem": 0.25
      },
      "colors": {
        "background": "#211a12",
        "gold": "#d5af68",
        "text": "#eee2ca",
        "health": "#668c46",
        "mana": "#478dbc",
        "stamina": "#bf9949"
      },
      "sample": {
        "className": "Warden",
        "cinders": 120,
        "act": 1,
        "floor": 4,
        "health": {
          "value": 32,
          "maximum": 40
        },
        "mana": {
          "value": 6,
          "maximum": 10
        },
        "stamina": {
          "value": 8,
          "maximum": 12
        },
        "experience": {
          "value": 40,
          "maximum": 100
        },
        "relics": [
          "Ash seal",
          "Ember charm"
        ],
        "potions": [
          "Smoke vial ×1"
        ],
        "chargeFlasks": [
          "HP ×2",
          "MP ×1"
        ]
      }
    },
    "minimumHudHeightRem": 5.5,
    "layerOrder": [
      {
        "id": "skyline",
        "z": 1
      },
      {
        "id": "floor",
        "z": 2
      },
      {
        "id": "actors",
        "z": 3
      },
      {
        "id": "targets",
        "z": 4
      },
      {
        "id": "context",
        "z": 5
      },
      {
        "id": "hud",
        "z": 6
      },
      {
        "id": "footer",
        "z": 6
      }
    ]
  },
  "hud": {
    "context": "combat",
    "enabled": true,
    "layoutPreset": "proposed",
    "layers": {
      "header": true,
      "class": true,
      "cinders": true,
      "position": true,
      "vitality": true,
      "health": true,
      "mana": true,
      "stamina": true,
      "armoury": true,
      "menu": true,
      "rail": true,
      "relics": true,
      "potions": true,
      "experience": true,
      "chargeFlasks": true
    },
    "vitality": {
      "referenceMaximum": {
        "health": 200,
        "mana": 20,
        "stamina": 20
      },
      "maximumWidthPercent": 100,
      "scaleByMaximum": true
    },
    "potions": {
      "placement": "footerOnly",
      "componentId": "WGC11",
      "contentsComponentId": "WGH8",
      "openIntent": "openPotions",
      "combineChargeFlasks": true,
      "combineCarriedPotions": true
    },
    "experience": {
      "contexts": [
        "combat"
      ],
      "color": "#398bd1",
      "heightRem": 0.35,
      "animationMs": 650,
      "awardPreview": 15
    },
    "phone": {
      "gate": "data-layout='narrow'",
      "maxWidthPx": 520,
      "meters": "stacked",
      "layerOverrides": {
        "class": false,
        "position": false,
        "route": false
      }
    },
    "diagram": {
      "columns": {
        "wide": 62,
        "compact": 48,
        "portraitSE": 40,
        "portraitS24": 42
      },
      "trackColumns": 20,
      "labelColumns": 8
    },
    "layout": {
      "gapRem": 0.35,
      "insetRem": 0.5,
      "meterHeightRem": 1.15,
      "actionHeightRem": 2.75,
      "radiusRem": 0.25
    },
    "colors": {
      "background": "#211a12",
      "gold": "#d5af68",
      "text": "#eee2ca",
      "health": "#668c46",
      "mana": "#478dbc",
      "stamina": "#bf9949"
    },
    "sample": {
      "className": "Warden",
      "cinders": 120,
      "act": 1,
      "floor": 4,
      "health": {
        "value": 32,
        "maximum": 40
      },
      "mana": {
        "value": 6,
        "maximum": 10
      },
      "stamina": {
        "value": 8,
        "maximum": 10
      },
      "experience": {
        "value": 40,
        "maximum": 100
      },
      "relics": [
        "Ash seal",
        "Ember charm"
      ],
      "potions": [
        "Smoke vial ×1"
      ],
      "chargeFlasks": [
        "HP ×2",
        "MP ×1"
      ]
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W4a: Combat — tightly packed footer

**Parent: W4.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. A/End Turn/P are largest and raised; pile buttons smaller; gaps minimal in every mode. No space-between. Empty resources/piles fade; End Turn does not and turns green when legal with zero actions. Proposed 10% HUD / 40% battlefield / 35% hand / 15% footer is the latest discussed starting allocation, subject to readability/playability verification. Layer stack, top first: effects and pointers z7 (code only, always on), HUD and footer z6, hand z5, targets z4, actors z3, floor z2, skyline z1.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ Identity · Resources                    [Menu] │
│                                                │
│ Battlefield + intentions                       │
│                                                │
│ Hand                                           │
│                                                │
│             (A)[D][END TURN][E](P)             │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4a.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4a.header` | 100vw | 10vh | W4a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4a.header.title` | 82vw | 6vh | W4a.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4a.header.exit` | 8vw | 6vh | W4a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4a.body` | 100vw | 75vh | W4a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4a.scene` | 100vw | 40vh | W4a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4a.context` | 100vw | 35vh | W4a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4a.scene.battlefield` | 95vw | 36vh | W4a.scene | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Art and targets fit inside scene |
| `W4a.context.hand` | 95vw | 31vh | W4a.context | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Playing-card host allocation, not standalone card size |
| `W4a.footer` | 100vw | 15vh | W4a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4a.footer.actionsRemaining` | 8vw | 8vh | W4a.footer.group | column 1 | center / center | single packed row; never distribute across viewport | 0.5vw gap; lifted 1vh above group bottom | Circle; large, group left |
| `W4a.footer.drawPile` | 5.6vw | 6vh | W4a.footer.group | column 2 | center / center | single packed row; never distribute across viewport | 0.5vw gap; bottom aligned in group | Small |
| `W4a.footer.endTurn` | 12vw | 8vh | W4a.footer.group | column 3 (center) | center / center | single packed row; never distribute across viewport | 0.5vw gap; lifted 1vh above group bottom | Large, group center |
| `W4a.footer.discardExhaust` | 5.6vw | 6vh | W4a.footer.group | column 4 | center / center | single packed row; never distribute across viewport | 0.5vw gap; bottom aligned in group | Small |
| `W4a.footer.potions` | 8vw | 8vh | W4a.footer.group | column 5 | center / center | single packed row; never distribute across viewport | 0.5vw gap; lifted 1vh above group bottom | Circle; large, group right |
| `W4a.footer.group` | 41.2vw | 11vh | W4a.footer | bottom-center | center / center | single packed row; never distribute across viewport | centered horizontally; 2vh above footer bottom | Four 0.5vw nominal gaps; packed centered, never stretched |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Compact HUD               [Menu] │
│                                  │
│ Battlefield + intentions         │
│                                  │
│ Hand                             │
│                                  │
│      (A)[D][END TURN][E](P)      │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4a.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4a.header` | 100vw | 10vh | W4a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4a.header.title` | 82vw | 6vh | W4a.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4a.header.exit` | 8vw | 6vh | W4a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4a.body` | 100vw | 75vh | W4a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4a.scene` | 100vw | 40vh | W4a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4a.context` | 100vw | 35vh | W4a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4a.scene.battlefield` | 95vw | 36vh | W4a.scene | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Art and targets fit inside scene |
| `W4a.context.hand` | 95vw | 31vh | W4a.context | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Playing-card host allocation, not standalone card size |
| `W4a.footer` | 100vw | 15vh | W4a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4a.footer.actionsRemaining` | 10vw | 8vh | W4a.footer.group | column 1 | center / center | single packed row; never distribute across viewport | 0.5vw gap; lifted 1vh above group bottom | Circle; large, group left |
| `W4a.footer.drawPile` | 7vw | 6vh | W4a.footer.group | column 2 | center / center | single packed row; never distribute across viewport | 0.5vw gap; bottom aligned in group | Small |
| `W4a.footer.endTurn` | 15vw | 8vh | W4a.footer.group | column 3 (center) | center / center | single packed row; never distribute across viewport | 0.5vw gap; lifted 1vh above group bottom | Large, group center |
| `W4a.footer.discardExhaust` | 7vw | 6vh | W4a.footer.group | column 4 | center / center | single packed row; never distribute across viewport | 0.5vw gap; bottom aligned in group | Small |
| `W4a.footer.potions` | 10vw | 8vh | W4a.footer.group | column 5 | center / center | single packed row; never distribute across viewport | 0.5vw gap; lifted 1vh above group bottom | Circle; large, group right |
| `W4a.footer.group` | 51vw | 11vh | W4a.footer | bottom-center | center / center | single packed row; never distribute across viewport | centered horizontally; 2vh above footer bottom | Four 0.5vw nominal gaps; packed centered, never stretched |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Compact HUD        [Menu] │
│                           │
│ Battlefield + intentions  │
│                           │
│                           │
│                           │
│ Hand                      │
│                           │
│  (A)[D][END TURN][E](P)   │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4a.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4a.header` | 100vw | 10vh | W4a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4a.header.title` | 82vw | 6vh | W4a.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4a.header.exit` | 8vw | 6vh | W4a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4a.body` | 100vw | 75vh | W4a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4a.scene` | 100vw | 40vh | W4a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4a.context` | 100vw | 35vh | W4a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4a.scene.battlefield` | 95vw | 36vh | W4a.scene | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Art and targets fit inside scene |
| `W4a.context.hand` | 95vw | 31vh | W4a.context | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Playing-card host allocation, not standalone card size |
| `W4a.footer` | 100vw | 15vh | W4a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4a.footer.actionsRemaining` | 13vw | 8vh | W4a.footer.group | column 1 | center / center | single packed row; never distribute across viewport | 0.5vw gap; lifted 1vh above group bottom | Circle; large, group left |
| `W4a.footer.drawPile` | 9.1vw | 6vh | W4a.footer.group | column 2 | center / center | single packed row; never distribute across viewport | 0.5vw gap; bottom aligned in group | Small |
| `W4a.footer.endTurn` | 19.5vw | 8vh | W4a.footer.group | column 3 (center) | center / center | single packed row; never distribute across viewport | 0.5vw gap; lifted 1vh above group bottom | Large, group center |
| `W4a.footer.discardExhaust` | 9.1vw | 6vh | W4a.footer.group | column 4 | center / center | single packed row; never distribute across viewport | 0.5vw gap; bottom aligned in group | Small |
| `W4a.footer.potions` | 13vw | 8vh | W4a.footer.group | column 5 | center / center | single packed row; never distribute across viewport | 0.5vw gap; lifted 1vh above group bottom | Circle; large, group right |
| `W4a.footer.group` | 65.7vw | 11vh | W4a.footer | bottom-center | center / center | single packed row; never distribute across viewport | centered horizontally; 2vh above footer bottom | Four 0.5vw nominal gaps; packed centered, never stretched |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Compact HUD        [Menu] │
│                           │
│ Battlefield + intentions  │
│                           │
│                           │
│                           │
│ Hand                      │
│                           │
│  (A)[D][END TURN][E](P)   │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4a.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4a.header` | 100vw | 10vh | W4a.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4a.header.title` | 82vw | 6vh | W4a.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4a.header.exit` | 8vw | 6vh | W4a.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4a.body` | 100vw | 75vh | W4a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4a.scene` | 100vw | 40vh | W4a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4a.context` | 100vw | 35vh | W4a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4a.scene.battlefield` | 95vw | 36vh | W4a.scene | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Art and targets fit inside scene |
| `W4a.context.hand` | 95vw | 31vh | W4a.context | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Playing-card host allocation, not standalone card size |
| `W4a.footer` | 100vw | 15vh | W4a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4a.footer.actionsRemaining` | 13vw | 8vh | W4a.footer.group | column 1 | center / center | single packed row; never distribute across viewport | 0.5vw gap; lifted 1vh above group bottom | Circle; large, group left |
| `W4a.footer.drawPile` | 9.1vw | 6vh | W4a.footer.group | column 2 | center / center | single packed row; never distribute across viewport | 0.5vw gap; bottom aligned in group | Small |
| `W4a.footer.endTurn` | 19.5vw | 8vh | W4a.footer.group | column 3 (center) | center / center | single packed row; never distribute across viewport | 0.5vw gap; lifted 1vh above group bottom | Large, group center |
| `W4a.footer.discardExhaust` | 9.1vw | 6vh | W4a.footer.group | column 4 | center / center | single packed row; never distribute across viewport | 0.5vw gap; bottom aligned in group | Small |
| `W4a.footer.potions` | 13vw | 8vh | W4a.footer.group | column 5 | center / center | single packed row; never distribute across viewport | 0.5vw gap; lifted 1vh above group bottom | Circle; large, group right |
| `W4a.footer.group` | 65.7vw | 11vh | W4a.footer | bottom-center | center / center | single packed row; never distribute across viewport | centered horizontally; 2vh above footer bottom | Four 0.5vw nominal gaps; packed centered, never stretched |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: committedCombatSnapshot, enginePreviewAPI, localTargetSelection
PARENT: W4
model.scene = BattlefieldIntentAndStatusProjection()
model.context = HandAndTargetingProjection()
model.footerPlacement = packedCombat
model.actions = [ActionsRemaining, DrawPile, EndTurn, DiscardExhaust, Potions]
model.footerSizes = large(ActionsRemaining, EndTurn, Potions), small(pileButtons)
ComposeW4Regions(
    WGS2.SharedHUD(model.hud),
    WGC1.Battlefield(model.battlefield),
    WGC5.Hand(model.hand),
    WGC6.CombatFooter(model.footer)
)
// Standalone previews and W4a call the same HUD/footer renderers.
CenterGroupWithMinimalSharedGapsInEveryMode()
EmptyResourceOrPileIndicatorsFade(); EndTurnNeverUsesEmptyFade()
EndTurnGreen = canEndTurn AND (NoActionsRemain(actionsRemaining) OR highlightedEndTurn)
ON DrawPile: OpenReadOnlyDrawViewer()
ON DiscardExhaust: OpenW1h()
ON EndTurn: DispatchExistingEngineOrServerCommandIfLegal()
ON card/target/potion intent: UseExistingValidatedCombatBindings()
The discussed config.combat.regionFractions region split is a starting allocation to verify.
Preserve RNG calls, simulation timing, combat saves, and server authority.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/screens/map.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/map.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/screens/event.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGS1](wireframe-gallery.html#WGS1), [WGS2](wireframe-gallery.html#WGS2), [WGS3](wireframe-gallery.html#WGS3), [WGS4](wireframe-gallery.html#WGS4), [WGS5](wireframe-gallery.html#WGS5), [WGC1](wireframe-gallery.html#WGC1), [WGC2](wireframe-gallery.html#WGC2), [WGC3](wireframe-gallery.html#WGC3), [WGC4](wireframe-gallery.html#WGC4), [WGC5](wireframe-gallery.html#WGC5), [WGC6](wireframe-gallery.html#WGC6), [WGC7](wireframe-gallery.html#WGC7), [WGC8](wireframe-gallery.html#WGC8), [WGC9](wireframe-gallery.html#WGC9), [WGC10](wireframe-gallery.html#WGC10), [WGC11](wireframe-gallery.html#WGC11), [WGS6](wireframe-gallery.html#WGS6), [WGS7](wireframe-gallery.html#WGS7), [WGH1](wireframe-gallery.html#WGH1), [WGH2](wireframe-gallery.html#WGH2), [WGH3](wireframe-gallery.html#WGH3), [WGH4](wireframe-gallery.html#WGH4), [WGH5](wireframe-gallery.html#WGH5), [WGH6](wireframe-gallery.html#WGH6), [WGH7](wireframe-gallery.html#WGH7), [WGH8](wireframe-gallery.html#WGH8)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [component-completion-client.js](component-completion-client.js), [hud-reference-client.js](hud-reference-client.js), [scene-reference-client.js](scene-reference-client.js), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `gameplayPreview`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [component-completion.css](component-completion.css), [scene-reference.css](scene-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "context": "W4a",
  "scene": {
    "parent": "W4",
    "bands": {
      "hud": 10,
      "scene": 55,
      "context": 30,
      "footer": 5
    },
    "layers": {
      "skyline": true,
      "floor": true,
      "hud": true,
      "actors": true,
      "targets": true,
      "context": true,
      "footer": true
    },
    "playerCount": 6,
    "enemyCount": 6,
    "components": [
      "WGS1",
      "WGS2",
      "WGS3",
      "WGS4",
      "WGS5",
      "WGC1",
      "WGC2",
      "WGC3",
      "WGC4",
      "WGC5",
      "WGC6",
      "WGC7",
      "WGC8",
      "WGC9",
      "WGC10",
      "WGC11",
      "WGS6",
      "WGS7",
      "WGH1",
      "WGH2",
      "WGH3",
      "WGH4",
      "WGH5",
      "WGH6",
      "WGH7",
      "WGH8"
    ],
    "background": {
      "floorHeightPercent": 80,
      "skylineColor": "#635139",
      "floorColor": "#382c20"
    },
    "hud": {
      "context": "combat",
      "enabled": true,
      "layoutPreset": "proposed",
      "layers": {
        "header": true,
        "class": true,
        "cinders": true,
        "position": true,
        "vitality": true,
        "health": true,
        "mana": true,
        "stamina": false,
        "armoury": true,
        "menu": true,
        "rail": false,
        "relics": true,
        "potions": true,
        "experience": true,
        "chargeFlasks": false
      },
      "experience": {
        "contexts": [
          "combat"
        ],
        "color": "#398bd1",
        "heightRem": 0.35,
        "animationMs": 650,
        "awardPreview": 15
      },
      "layout": {
        "gapRem": 0.15,
        "insetRem": 0.15,
        "meterHeightRem": 0.8,
        "actionHeightRem": 2.75,
        "radiusRem": 0.25
      },
      "colors": {
        "background": "#211a12",
        "gold": "#d5af68",
        "text": "#eee2ca",
        "health": "#668c46",
        "mana": "#478dbc",
        "stamina": "#bf9949"
      },
      "sample": {
        "className": "Warden",
        "cinders": 120,
        "act": 1,
        "floor": 4,
        "health": {
          "value": 32,
          "maximum": 40
        },
        "mana": {
          "value": 6,
          "maximum": 10
        },
        "stamina": {
          "value": 8,
          "maximum": 12
        },
        "experience": {
          "value": 40,
          "maximum": 100
        },
        "relics": [
          "Ash seal",
          "Ember charm"
        ],
        "potions": [
          "Smoke vial ×1"
        ],
        "chargeFlasks": [
          "HP ×2",
          "MP ×1"
        ]
      }
    },
    "minimumHudHeightRem": 5.5,
    "layerOrder": [
      {
        "id": "skyline",
        "z": 1
      },
      {
        "id": "floor",
        "z": 2
      },
      {
        "id": "actors",
        "z": 3
      },
      {
        "id": "targets",
        "z": 4
      },
      {
        "id": "context",
        "z": 5
      },
      {
        "id": "hud",
        "z": 6
      },
      {
        "id": "footer",
        "z": 6
      }
    ]
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "scene": {
    "parent": "W4",
    "bands": {
      "hud": 10,
      "scene": 55,
      "context": 30,
      "footer": 5
    },
    "layers": {
      "skyline": true,
      "floor": true,
      "hud": true,
      "actors": true,
      "targets": true,
      "context": true,
      "footer": true
    },
    "playerCount": 6,
    "enemyCount": 6,
    "components": [
      "WGS1",
      "WGS2",
      "WGS3",
      "WGS4",
      "WGS5",
      "WGC1",
      "WGC2",
      "WGC3",
      "WGC4",
      "WGC5",
      "WGC6",
      "WGC7",
      "WGC8",
      "WGC9",
      "WGC10",
      "WGC11",
      "WGS6",
      "WGS7",
      "WGH1",
      "WGH2",
      "WGH3",
      "WGH4",
      "WGH5",
      "WGH6",
      "WGH7",
      "WGH8"
    ],
    "background": {
      "floorHeightPercent": 80,
      "skylineColor": "#635139",
      "floorColor": "#382c20"
    },
    "hud": {
      "context": "combat",
      "enabled": true,
      "layoutPreset": "proposed",
      "layers": {
        "header": true,
        "class": true,
        "cinders": true,
        "position": true,
        "vitality": true,
        "health": true,
        "mana": true,
        "stamina": false,
        "armoury": true,
        "menu": true,
        "rail": false,
        "relics": true,
        "potions": true,
        "experience": true,
        "chargeFlasks": false
      },
      "experience": {
        "contexts": [
          "combat"
        ],
        "color": "#398bd1",
        "heightRem": 0.35,
        "animationMs": 650,
        "awardPreview": 15
      },
      "layout": {
        "gapRem": 0.15,
        "insetRem": 0.15,
        "meterHeightRem": 0.8,
        "actionHeightRem": 2.75,
        "radiusRem": 0.25
      },
      "colors": {
        "background": "#211a12",
        "gold": "#d5af68",
        "text": "#eee2ca",
        "health": "#668c46",
        "mana": "#478dbc",
        "stamina": "#bf9949"
      },
      "sample": {
        "className": "Warden",
        "cinders": 120,
        "act": 1,
        "floor": 4,
        "health": {
          "value": 32,
          "maximum": 40
        },
        "mana": {
          "value": 6,
          "maximum": 10
        },
        "stamina": {
          "value": 8,
          "maximum": 12
        },
        "experience": {
          "value": 40,
          "maximum": 100
        },
        "relics": [
          "Ash seal",
          "Ember charm"
        ],
        "potions": [
          "Smoke vial ×1"
        ],
        "chargeFlasks": [
          "HP ×2",
          "MP ×1"
        ]
      }
    },
    "minimumHudHeightRem": 5.5,
    "layerOrder": [
      {
        "id": "skyline",
        "z": 1
      },
      {
        "id": "floor",
        "z": 2
      },
      {
        "id": "actors",
        "z": 3
      },
      {
        "id": "targets",
        "z": 4
      },
      {
        "id": "context",
        "z": 5
      },
      {
        "id": "hud",
        "z": 6
      },
      {
        "id": "footer",
        "z": 6
      }
    ]
  },
  "hud": {
    "context": "combat",
    "enabled": true,
    "layoutPreset": "proposed",
    "layers": {
      "header": true,
      "class": true,
      "cinders": true,
      "position": true,
      "vitality": true,
      "health": true,
      "mana": true,
      "stamina": true,
      "armoury": true,
      "menu": true,
      "rail": true,
      "relics": true,
      "potions": true,
      "experience": true,
      "chargeFlasks": true
    },
    "vitality": {
      "referenceMaximum": {
        "health": 200,
        "mana": 20,
        "stamina": 20
      },
      "maximumWidthPercent": 100,
      "scaleByMaximum": true
    },
    "potions": {
      "placement": "footerOnly",
      "componentId": "WGC11",
      "contentsComponentId": "WGH8",
      "openIntent": "openPotions",
      "combineChargeFlasks": true,
      "combineCarriedPotions": true
    },
    "experience": {
      "contexts": [
        "combat"
      ],
      "color": "#398bd1",
      "heightRem": 0.35,
      "animationMs": 650,
      "awardPreview": 15
    },
    "phone": {
      "gate": "data-layout='narrow'",
      "maxWidthPx": 520,
      "meters": "stacked",
      "layerOverrides": {
        "class": false,
        "position": false,
        "route": false
      }
    },
    "diagram": {
      "columns": {
        "wide": 62,
        "compact": 48,
        "portraitSE": 40,
        "portraitS24": 42
      },
      "trackColumns": 20,
      "labelColumns": 8
    },
    "layout": {
      "gapRem": 0.35,
      "insetRem": 0.5,
      "meterHeightRem": 1.15,
      "actionHeightRem": 2.75,
      "radiusRem": 0.25
    },
    "colors": {
      "background": "#211a12",
      "gold": "#d5af68",
      "text": "#eee2ca",
      "health": "#668c46",
      "mana": "#478dbc",
      "stamina": "#bf9949"
    },
    "sample": {
      "className": "Warden",
      "cinders": 120,
      "act": 1,
      "floor": 4,
      "health": {
        "value": 32,
        "maximum": 40
      },
      "mana": {
        "value": 6,
        "maximum": 10
      },
      "stamina": {
        "value": 8,
        "maximum": 10
      },
      "experience": {
        "value": 40,
        "maximum": 100
      },
      "relics": [
        "Ash seal",
        "Ember charm"
      ],
      "potions": [
        "Smoke vial ×1"
      ],
      "chargeFlasks": [
        "HP ×2",
        "MP ×1"
      ]
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W4b: Map — 10 / 60 / 20 / 10

**Parent: W4.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Normal height budget is 10/60/20/10, with spacing inside the bands and viewport/zoom conversion centralized. Preserve minimum readable/control sizes; map yields only where required. No combat footer. Layer stack, top first: HUD and footer z6, node details z5, nodes z4, paths z3, floor z2, skyline z1.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ HUD · [Region ▾]                 [Menu] 10%    │
├────────────────────────────────────────────────┤
│ ┌────── Map ≈95% viewport width ──────┐        │
│ │            MAP / NODES             │         │
│ │                               60% │          │
│ └───────────────────────────────────┘          │
├────────────────────────────────────────────────┤
│ Selected node · Known facts         20%        │
│ Relevant detail / risk / blocker               │
├────────────────────────────────────────────────┤
│ [Recenter]                             [Enter] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4b.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4b.header` | 100vw | 10vh | W4b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4b.header.title` | 82vw | 6vh | W4b.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4b.header.exit` | 8vw | 6vh | W4b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4b.body` | 100vw | 80vh | W4b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4b.scene` | 100vw | 60vh | W4b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4b.context` | 100vw | 20vh | W4b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4b.scene.map` | 95vw | 60vh | W4b.scene | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Owner target ≈60vh ×95vw |
| `W4b.footer` | 100vw | 10vh | W4b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4b.footer.closeBack` | 46.5vw | 6vh | W4b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W4b.footer.primary` | 46.5vw | 6vh | W4b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W4b.footer.singleAction` | 95vw | 6vh | W4b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ HUD · [Region ▾] [Menu]      10% │
├──────────────────────────────────┤
│ [MAP / NODES]                    │
│ Width ≈95%                  60%  │
├──────────────────────────────────┤
│ Node · Facts · Blocker      20%  │
├──────────────────────────────────┤
│ [Recenter]               [Enter] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4b.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4b.header` | 100vw | 10vh | W4b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4b.header.title` | 82vw | 6vh | W4b.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4b.header.exit` | 8vw | 6vh | W4b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4b.body` | 100vw | 80vh | W4b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4b.scene` | 100vw | 60vh | W4b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4b.context` | 100vw | 20vh | W4b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4b.scene.map` | 95vw | 60vh | W4b.scene | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Owner target ≈60vh ×95vw |
| `W4b.footer` | 100vw | 10vh | W4b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4b.footer.closeBack` | 46.5vw | 6vh | W4b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W4b.footer.primary` | 46.5vw | 6vh | W4b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W4b.footer.singleAction` | 95vw | 6vh | W4b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ HUD [Region ▾][Menu]  10% │
├───────────────────────────┤
│ [MAP / NODES]             │
│                           │
│ Width ≈95%                │
│                           │
│                      60%  │
│                           │
├───────────────────────────┤
│ Selected node             │
│ Known facts / blocker 20% │
├───────────────────────────┤
│ [Recenter]        [Enter] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4b.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4b.header` | 100vw | 10vh | W4b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4b.header.title` | 82vw | 6vh | W4b.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4b.header.exit` | 8vw | 6vh | W4b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4b.body` | 100vw | 80vh | W4b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4b.scene` | 100vw | 60vh | W4b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4b.context` | 100vw | 20vh | W4b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4b.scene.map` | 95vw | 60vh | W4b.scene | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Owner target ≈60vh ×95vw |
| `W4b.footer` | 100vw | 10vh | W4b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4b.footer.closeBack` | 46.5vw | 6vh | W4b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W4b.footer.primary` | 46.5vw | 6vh | W4b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W4b.footer.singleAction` | 95vw | 6vh | W4b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ HUD [Region ▾][Menu]  10% │
├───────────────────────────┤
│ [MAP / NODES]             │
│                           │
│ Width ≈95%                │
│                           │
│                      60%  │
│                           │
├───────────────────────────┤
│ Selected node             │
│ Known facts / blocker 20% │
├───────────────────────────┤
│ [Recenter]        [Enter] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4b.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4b.header` | 100vw | 10vh | W4b.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4b.header.title` | 82vw | 6vh | W4b.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4b.header.exit` | 8vw | 6vh | W4b.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4b.body` | 100vw | 80vh | W4b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4b.scene` | 100vw | 60vh | W4b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4b.context` | 100vw | 20vh | W4b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4b.scene.map` | 95vw | 60vh | W4b.scene | center | center / center | normal grid flow | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Owner target ≈60vh ×95vw |
| `W4b.footer` | 100vw | 10vh | W4b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4b.footer.closeBack` | 46.5vw | 6vh | W4b.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W4b.footer.primary` | 46.5vw | 6vh | W4b.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W4b.footer.singleAction` | 95vw | 6vh | W4b.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: mapSnapshot, revealRules, selectedNodeId, viewport
PARENT: W4
model.regionFractions = config.map.regionFractions
model.mapInlineFraction = config.map.inlineFraction
model.header = HUDIncludingRegionSelector()
model.scene = RevealedMapAndReachabilityProjection()
model.context = KnownSelectedNodeFactsAndBlockers()
model.actions = [Recenter, EnterSelectedDestination]
RenderWithW4(model); KeepPaddingInsideHeightBudget()
ON selectNode: UpdateLocalPreviewWithoutEnteringOrRevealingHiddenFacts()
ON Recenter: DispatchViewOnlyMapCameraIntent()
ON Enter: RevalidateReachabilityAndDispatchExistingNodeEntryCommand()
ON constrainedTextOrHeight: HonorInputMinimumsAndControlledDetailsOverflow()
Never add combat footer controls or a fifth region-selector band.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/screens/map.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/map.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/screens/event.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGS1](wireframe-gallery.html#WGS1), [WGS2](wireframe-gallery.html#WGS2), [WGS3](wireframe-gallery.html#WGS3), [WGS4](wireframe-gallery.html#WGS4), [WGS5](wireframe-gallery.html#WGS5), [WGM1](wireframe-gallery.html#WGM1), [WGM2](wireframe-gallery.html#WGM2), [WGM3](wireframe-gallery.html#WGM3), [WGM4](wireframe-gallery.html#WGM4), [WGM5](wireframe-gallery.html#WGM5), [WGM6](wireframe-gallery.html#WGM6), [WGM7](wireframe-gallery.html#WGM7), [WGS6](wireframe-gallery.html#WGS6), [WGS7](wireframe-gallery.html#WGS7), [WGH1](wireframe-gallery.html#WGH1), [WGH2](wireframe-gallery.html#WGH2), [WGH3](wireframe-gallery.html#WGH3), [WGH4](wireframe-gallery.html#WGH4), [WGH5](wireframe-gallery.html#WGH5), [WGH6](wireframe-gallery.html#WGH6), [WGH7](wireframe-gallery.html#WGH7), [WGH8](wireframe-gallery.html#WGH8)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [component-completion-client.js](component-completion-client.js), [hud-reference-client.js](hud-reference-client.js), [scene-reference-client.js](scene-reference-client.js)

Renderer binding: `gameplayPreview`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [component-completion.css](component-completion.css), [scene-reference.css](scene-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "context": "W4b",
  "scene": {
    "parent": "W4",
    "bands": {
      "hud": 10,
      "scene": 60,
      "context": 20,
      "footer": 10
    },
    "mapWidth": 95,
    "layers": {
      "skyline": true,
      "floor": true,
      "hud": true,
      "paths": true,
      "nodes": true,
      "context": true,
      "footer": true
    },
    "components": [
      "WGS1",
      "WGS2",
      "WGS3",
      "WGS4",
      "WGS5",
      "WGM1",
      "WGM2",
      "WGM3",
      "WGM4",
      "WGM5",
      "WGM6",
      "WGM7",
      "WGS6",
      "WGS7",
      "WGH1",
      "WGH2",
      "WGH3",
      "WGH4",
      "WGH5",
      "WGH6",
      "WGH7",
      "WGH8"
    ],
    "background": {
      "floorHeightPercent": 35,
      "skylineColor": "#635139",
      "floorColor": "#382c20"
    },
    "hud": {
      "context": "map",
      "enabled": true,
      "layoutPreset": "proposed",
      "layers": {
        "header": true,
        "class": true,
        "cinders": true,
        "position": true,
        "vitality": true,
        "health": true,
        "mana": true,
        "stamina": false,
        "armoury": true,
        "menu": true,
        "rail": false,
        "relics": true,
        "potions": true,
        "experience": true,
        "chargeFlasks": false
      },
      "experience": {
        "contexts": [
          "combat"
        ],
        "color": "#398bd1",
        "heightRem": 0.35,
        "animationMs": 650,
        "awardPreview": 15
      },
      "layout": {
        "gapRem": 0.15,
        "insetRem": 0.15,
        "meterHeightRem": 0.8,
        "actionHeightRem": 2.75,
        "radiusRem": 0.25
      },
      "colors": {
        "background": "#211a12",
        "gold": "#d5af68",
        "text": "#eee2ca",
        "health": "#668c46",
        "mana": "#478dbc",
        "stamina": "#bf9949"
      },
      "sample": {
        "className": "Warden",
        "cinders": 120,
        "act": 1,
        "floor": 4,
        "health": {
          "value": 32,
          "maximum": 40
        },
        "mana": {
          "value": 6,
          "maximum": 10
        },
        "stamina": {
          "value": 8,
          "maximum": 12
        },
        "experience": {
          "value": 40,
          "maximum": 100
        },
        "relics": [
          "Ash seal",
          "Ember charm"
        ],
        "potions": [
          "Smoke vial ×1"
        ],
        "chargeFlasks": [
          "HP ×2",
          "MP ×1"
        ]
      }
    },
    "minimumHudHeightRem": 5.5,
    "layerOrder": [
      {
        "id": "skyline",
        "z": 1
      },
      {
        "id": "floor",
        "z": 2
      },
      {
        "id": "paths",
        "z": 3
      },
      {
        "id": "nodes",
        "z": 4
      },
      {
        "id": "context",
        "z": 5
      },
      {
        "id": "hud",
        "z": 6
      },
      {
        "id": "footer",
        "z": 6
      }
    ]
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "scene": {
    "parent": "W4",
    "bands": {
      "hud": 10,
      "scene": 60,
      "context": 20,
      "footer": 10
    },
    "mapWidth": 95,
    "layers": {
      "skyline": true,
      "floor": true,
      "hud": true,
      "paths": true,
      "nodes": true,
      "context": true,
      "footer": true
    },
    "components": [
      "WGS1",
      "WGS2",
      "WGS3",
      "WGS4",
      "WGS5",
      "WGM1",
      "WGM2",
      "WGM3",
      "WGM4",
      "WGM5",
      "WGM6",
      "WGM7",
      "WGS6",
      "WGS7",
      "WGH1",
      "WGH2",
      "WGH3",
      "WGH4",
      "WGH5",
      "WGH6",
      "WGH7",
      "WGH8"
    ],
    "background": {
      "floorHeightPercent": 35,
      "skylineColor": "#635139",
      "floorColor": "#382c20"
    },
    "hud": {
      "context": "map",
      "enabled": true,
      "layoutPreset": "proposed",
      "layers": {
        "header": true,
        "class": true,
        "cinders": true,
        "position": true,
        "vitality": true,
        "health": true,
        "mana": true,
        "stamina": false,
        "armoury": true,
        "menu": true,
        "rail": false,
        "relics": true,
        "potions": true,
        "experience": true,
        "chargeFlasks": false
      },
      "experience": {
        "contexts": [
          "combat"
        ],
        "color": "#398bd1",
        "heightRem": 0.35,
        "animationMs": 650,
        "awardPreview": 15
      },
      "layout": {
        "gapRem": 0.15,
        "insetRem": 0.15,
        "meterHeightRem": 0.8,
        "actionHeightRem": 2.75,
        "radiusRem": 0.25
      },
      "colors": {
        "background": "#211a12",
        "gold": "#d5af68",
        "text": "#eee2ca",
        "health": "#668c46",
        "mana": "#478dbc",
        "stamina": "#bf9949"
      },
      "sample": {
        "className": "Warden",
        "cinders": 120,
        "act": 1,
        "floor": 4,
        "health": {
          "value": 32,
          "maximum": 40
        },
        "mana": {
          "value": 6,
          "maximum": 10
        },
        "stamina": {
          "value": 8,
          "maximum": 12
        },
        "experience": {
          "value": 40,
          "maximum": 100
        },
        "relics": [
          "Ash seal",
          "Ember charm"
        ],
        "potions": [
          "Smoke vial ×1"
        ],
        "chargeFlasks": [
          "HP ×2",
          "MP ×1"
        ]
      }
    },
    "minimumHudHeightRem": 5.5,
    "layerOrder": [
      {
        "id": "skyline",
        "z": 1
      },
      {
        "id": "floor",
        "z": 2
      },
      {
        "id": "paths",
        "z": 3
      },
      {
        "id": "nodes",
        "z": 4
      },
      {
        "id": "context",
        "z": 5
      },
      {
        "id": "hud",
        "z": 6
      },
      {
        "id": "footer",
        "z": 6
      }
    ]
  },
  "hud": {
    "context": "combat",
    "enabled": true,
    "layoutPreset": "proposed",
    "layers": {
      "header": true,
      "class": true,
      "cinders": true,
      "position": true,
      "vitality": true,
      "health": true,
      "mana": true,
      "stamina": true,
      "armoury": true,
      "menu": true,
      "rail": true,
      "relics": true,
      "potions": true,
      "experience": true,
      "chargeFlasks": true
    },
    "vitality": {
      "referenceMaximum": {
        "health": 200,
        "mana": 20,
        "stamina": 20
      },
      "maximumWidthPercent": 100,
      "scaleByMaximum": true
    },
    "potions": {
      "placement": "footerOnly",
      "componentId": "WGC11",
      "contentsComponentId": "WGH8",
      "openIntent": "openPotions",
      "combineChargeFlasks": true,
      "combineCarriedPotions": true
    },
    "experience": {
      "contexts": [
        "combat"
      ],
      "color": "#398bd1",
      "heightRem": 0.35,
      "animationMs": 650,
      "awardPreview": 15
    },
    "phone": {
      "gate": "data-layout='narrow'",
      "maxWidthPx": 520,
      "meters": "stacked",
      "layerOverrides": {
        "class": false,
        "position": false,
        "route": false
      }
    },
    "diagram": {
      "columns": {
        "wide": 62,
        "compact": 48,
        "portraitSE": 40,
        "portraitS24": 42
      },
      "trackColumns": 20,
      "labelColumns": 8
    },
    "layout": {
      "gapRem": 0.35,
      "insetRem": 0.5,
      "meterHeightRem": 1.15,
      "actionHeightRem": 2.75,
      "radiusRem": 0.25
    },
    "colors": {
      "background": "#211a12",
      "gold": "#d5af68",
      "text": "#eee2ca",
      "health": "#668c46",
      "mana": "#478dbc",
      "stamina": "#bf9949"
    },
    "sample": {
      "className": "Warden",
      "cinders": 120,
      "act": 1,
      "floor": 4,
      "health": {
        "value": 32,
        "maximum": 40
      },
      "mana": {
        "value": 6,
        "maximum": 10
      },
      "stamina": {
        "value": 8,
        "maximum": 10
      },
      "experience": {
        "value": 40,
        "maximum": 100
      },
      "relics": [
        "Ash seal",
        "Ember charm"
      ],
      "potions": [
        "Smoke vial ×1"
      ],
      "chargeFlasks": [
        "HP ×2",
        "MP ×1"
      ]
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe W4c: Quest dialogue — player left, NPC right

**Parent: W4.** Inherits the parent geometry, spacing, transitions, focus treatment, and lifecycle. Only view-model content, registered body slots, and declared action policies differ. Layer stack, bottom to top (layerOrder): the skybox z2 runs from the frame top to the floor line, behind the HUD; the floor z3 runs from the floor line (background.floorHeightPercent of the scene window is ground) to the frame bottom, behind the context and footer bands; the player and NPC portraits z4 are each the full figure zoomed so its top fraction (portraits.visibleFraction) spans the slot top to the reveal line, with the speaker drawn above the listener; the opaque context band z5, whose top edge is the reveal line, hides the figures below it, so nothing is cropped or masked; the HUD and footer bands z6 are the top panels. Every layer is switchable on its own (layers). Entrance (entrance): the scene, HUD and footer appear at once, the portraits fade in, then the context band fades in and rises; controls work once the last step ends, and reduced motion shows every layer at once. Context band contents (context): the quest title on one line, the narrative beat, then the responses; no eyebrow, speaker line or prompt hint, because the speaker is named under the speaking portrait. The band’s top padding clears the title’s line box (context.titleClearsLineBox), so no letter is clipped at any size. Up to four responses (context.maxVisibleResponses) show without scrolling, in a grid of context.responseColumns (two columns on wide and short landscape screens, one in portrait); the band scrolls only when there are more. Both portraits are visible at every size (portraits.alwaysVisible): each figure keeps to its lane, half the frame less the insets and the gap between them (the larger of portraits.minGapVw and portraits.minGapPx, so narrow screens keep the speakers apart), so the two never overlap; a figure wider than its lane shrinks as a whole (portraits.fit shrinkToLane), so wide screens keep the zoom and only narrow ones shrink, and sinks so its top share still stands on the reveal line (portraits.anchor), never hovering; the listener dims no further than portraits.listener. Portraits stay left and right even on mobile. Dialogue replaces the hand. Audio may advance linear speech only; Back/Skip never commit choices or repeat effects. Captions/manual navigation work without playable audio.

**Wide**

```text
┌────────────────────────────────────────────────┐
│ HUD · Resources                 [Menu]  z6     │
│ skybox z2 · runs behind the HUD                │
│     ╭──╮                          ╭──╮         │
│     │P │ Player z4       NPC z4 │ N│           │
│ ────┤  ├── floor line · floor z3 ┤  ├────      │
│    ╱    ╲                        ╱    ╲        │
│ ══ reveal line · context z5 hides the rest ══  │
│ Quest title                          [Voice Ⅱ] │
│ One short caption beat.                        │
│ [Response 1]          [Response 2]             │
│ [Response 3]          [Response 4]             │
│ [Back]              [Skip]          [Continue] │
└────────────────────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4c.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4c.header` | 100vw | 10vh | W4c.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4c.header.title` | 82vw | 6vh | W4c.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4c.header.exit` | 8vw | 6vh | W4c.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4c.body` | 100vw | 75vh | W4c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4c.scene` | 100vw | 40vh | W4c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4c.context` | 100vw | 35vh | W4c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4c.scene.skybox` | 100vw | 26vh | W4c.frame | frame top | stretch / start | layer z2 · absolute sheet, bottom of the stack | 0; runs behind the HUD band | Layer z2 · frame top → floor line; runs behind the HUD band |
| `W4c.scene.floor` | 100vw | 74vh | W4c.frame | floor line | stretch / start | layer z3 · absolute sheet | 0; runs behind the context and footer bands | Layer z3 · floor line at 26vh → frame bottom; runs behind the context and footer bands |
| `W4c.scene.playerPortrait` | 20vw | 114vh figure · 38vh visible | W4c.frame | left slot top | center / start | layer z4 · absolute; overruns the scene into lower layers | 2.5vw from respective frame edge; top 2vh below the scene top | Layer z4 · stays inside its 46.5vw lane; shrinks only when wider than the lane · full figure zoomed so the top 1/3 spans slot top (12vh) → reveal line; the rest is occluded by the context band, footer and frame edge, never cropped |
| `W4c.scene.npcPortrait` | 20vw | 114vh figure · 38vh visible | W4c.frame | right slot top | center / start | layer z4 · absolute; overruns the scene into lower layers | 2.5vw from respective frame edge; top 2vh below the scene top | Layer z4 · as the player, mirrored; lanes are half the frame less insets and a gap of 2vw or 24px, whichever is larger, so both are always visible; a figure wider than its 46.5vw lane shrinks and sinks to the reveal line; the speaker draws above the listener |
| `W4c.context.revealLine` | 100vw | edge at 50vh | W4c.context | context top edge | stretch / start | occluding edge of layer z5 | 0 | Top edge of the opaque context band; everything above it is visible |
| `W4c.context.dialogue` | 95vw | 31vh | W4c.context | center | center / center | inside layer z5 (opaque band) | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Layer z5 · opaque band; quest title, narrative, responses; no sub-headings |
| `W4c.context.questTitle` | 95vw | 1 line | W4c.context.dialogue | top-left | start / start | column row 1 | 2.5vw side inset; half gap to the band top | Replaces the eyebrow, speaker line and prompt hint |
| `W4c.context.narrative` | 95vw | ≤ 2 lines | W4c.context.dialogue | below quest title | start / start | column row 2 | half gap | One authored beat |
| `W4c.context.responses` | 95vw | 2 rows × min target | W4c.context.dialogue | below narrative | stretch / start | response grid; columns from config.context.responseColumns | half gap between rows and columns | 4 responses in 2 columns fit without scrolling; the band scrolls only past 4; each response wraps to 2 lines |
| `W4c.footer` | 100vw | 15vh | W4c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4c.footer.closeBack` | 46.5vw | 6vh | W4c.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W4c.footer.primary` | 46.5vw | 6vh | W4c.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W4c.footer.singleAction` | 95vw | 6vh | W4c.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |
| `W4c.footer.skipSpeech` | 30.67vw | 6vh | W4c.footer | bottom-center | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Three-action variant: each button takes one third minus shared gaps; replaces two-action widths |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Compact**

```text
┌──────────────────────────────────┐
│ Compact HUD · z6          [Menu] │
│ skybox z2                        │
│  ╭─╮                      ╭─╮    │
│  │P│ Player z4   NPC z4  │N│     │
│ ─┤ ├──── floor z3 ───────┤ ├─    │
│ ═ reveal line · context z5 ═══   │
│ Quest title            [Voice Ⅱ] │
│ One short caption beat.          │
│ [Response 1]  [Response 2]       │
│ [Response 3]  [Response 4]       │
│ [Back]       [Skip]   [Continue] │
└──────────────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4c.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4c.header` | 100vw | 10vh | W4c.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4c.header.title` | 82vw | 6vh | W4c.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4c.header.exit` | 8vw | 6vh | W4c.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4c.body` | 100vw | 75vh | W4c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4c.scene` | 100vw | 40vh | W4c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4c.context` | 100vw | 35vh | W4c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4c.scene.skybox` | 100vw | 26vh | W4c.frame | frame top | stretch / start | layer z2 · absolute sheet, bottom of the stack | 0; runs behind the HUD band | Layer z2 · frame top → floor line; runs behind the HUD band |
| `W4c.scene.floor` | 100vw | 74vh | W4c.frame | floor line | stretch / start | layer z3 · absolute sheet | 0; runs behind the context and footer bands | Layer z3 · floor line at 26vh → frame bottom; runs behind the context and footer bands |
| `W4c.scene.playerPortrait` | 30vw | 114vh figure · 38vh visible | W4c.frame | left slot top | center / start | layer z4 · absolute; overruns the scene into lower layers | 2.5vw from respective frame edge; top 2vh below the scene top | Layer z4 · stays inside its 46.5vw lane; shrinks only when wider than the lane · full figure zoomed so the top 1/3 spans slot top (12vh) → reveal line; the rest is occluded by the context band, footer and frame edge, never cropped |
| `W4c.scene.npcPortrait` | 30vw | 114vh figure · 38vh visible | W4c.frame | right slot top | center / start | layer z4 · absolute; overruns the scene into lower layers | 2.5vw from respective frame edge; top 2vh below the scene top | Layer z4 · as the player, mirrored; lanes are half the frame less insets and a gap of 2vw or 24px, whichever is larger, so both are always visible; a figure wider than its 46.5vw lane shrinks and sinks to the reveal line; the speaker draws above the listener |
| `W4c.context.revealLine` | 100vw | edge at 50vh | W4c.context | context top edge | stretch / start | occluding edge of layer z5 | 0 | Top edge of the opaque context band; everything above it is visible |
| `W4c.context.dialogue` | 95vw | 31vh | W4c.context | center | center / center | inside layer z5 (opaque band) | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Layer z5 · opaque band; quest title, narrative, responses; no sub-headings |
| `W4c.context.questTitle` | 95vw | 1 line | W4c.context.dialogue | top-left | start / start | column row 1 | 2.5vw side inset; half gap to the band top | Replaces the eyebrow, speaker line and prompt hint |
| `W4c.context.narrative` | 95vw | ≤ 2 lines | W4c.context.dialogue | below quest title | start / start | column row 2 | half gap | One authored beat |
| `W4c.context.responses` | 95vw | 2 rows × min target | W4c.context.dialogue | below narrative | stretch / start | response grid; columns from config.context.responseColumns | half gap between rows and columns | 4 responses in 2 columns fit without scrolling; the band scrolls only past 4; each response wraps to 2 lines |
| `W4c.footer` | 100vw | 15vh | W4c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4c.footer.closeBack` | 46.5vw | 6vh | W4c.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W4c.footer.primary` | 46.5vw | 6vh | W4c.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W4c.footer.singleAction` | 95vw | 6vh | W4c.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |
| `W4c.footer.skipSpeech` | 30.67vw | 6vh | W4c.footer | bottom-center | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Three-action variant: each button takes one third minus shared gaps; replaces two-action widths |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Compact HUD · z6   [Menu] │
│ skybox z2                 │
│  ╭─╮             ╭─╮      │
│  │P│ z4       z4 │N│      │
│ ─┤ ├── floor z3 ┤ ├─      │
│ both always visible       │
│ ═ reveal line · z5 ═══    │
│ Quest title     [Voice Ⅱ] │
│ One short caption beat.   │
│ [Response 1]              │
│ [Response 2]              │
│ [Response 3]              │
│ [Response 4]              │
│ [Back]   [Skip][Continue] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4c.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4c.header` | 100vw | 10vh | W4c.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4c.header.title` | 82vw | 6vh | W4c.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4c.header.exit` | 8vw | 6vh | W4c.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4c.body` | 100vw | 75vh | W4c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4c.scene` | 100vw | 40vh | W4c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4c.context` | 100vw | 35vh | W4c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4c.scene.skybox` | 100vw | 26vh | W4c.frame | frame top | stretch / start | layer z2 · absolute sheet, bottom of the stack | 0; runs behind the HUD band | Layer z2 · frame top → floor line; runs behind the HUD band |
| `W4c.scene.floor` | 100vw | 74vh | W4c.frame | floor line | stretch / start | layer z3 · absolute sheet | 0; runs behind the context and footer bands | Layer z3 · floor line at 26vh → frame bottom; runs behind the context and footer bands |
| `W4c.scene.playerPortrait` | 30vw | 114vh figure · 38vh visible | W4c.frame | left slot top | center / start | layer z4 · absolute; overruns the scene into lower layers | 2.5vw from respective frame edge; top 2vh below the scene top | Layer z4 · stays inside its 46.5vw lane; shrinks only when wider than the lane · full figure zoomed so the top 1/3 spans slot top (12vh) → reveal line; the rest is occluded by the context band, footer and frame edge, never cropped |
| `W4c.scene.npcPortrait` | 30vw | 114vh figure · 38vh visible | W4c.frame | right slot top | center / start | layer z4 · absolute; overruns the scene into lower layers | 2.5vw from respective frame edge; top 2vh below the scene top | Layer z4 · as the player, mirrored; lanes are half the frame less insets and a gap of 2vw or 24px, whichever is larger, so both are always visible; a figure wider than its 46.5vw lane shrinks and sinks to the reveal line; the speaker draws above the listener |
| `W4c.context.revealLine` | 100vw | edge at 50vh | W4c.context | context top edge | stretch / start | occluding edge of layer z5 | 0 | Top edge of the opaque context band; everything above it is visible |
| `W4c.context.dialogue` | 95vw | 31vh | W4c.context | center | center / center | inside layer z5 (opaque band) | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Layer z5 · opaque band; quest title, narrative, responses; no sub-headings |
| `W4c.context.questTitle` | 95vw | 1 line | W4c.context.dialogue | top-left | start / start | column row 1 | 2.5vw side inset; half gap to the band top | Replaces the eyebrow, speaker line and prompt hint |
| `W4c.context.narrative` | 95vw | ≤ 2 lines | W4c.context.dialogue | below quest title | start / start | column row 2 | half gap | One authored beat |
| `W4c.context.responses` | 95vw | 4 rows × min target | W4c.context.dialogue | below narrative | stretch / start | response grid; columns from config.context.responseColumns | half gap between rows and columns | 4 responses in 1 column fit without scrolling; the band scrolls only past 4; each response wraps to 2 lines |
| `W4c.footer` | 100vw | 15vh | W4c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4c.footer.closeBack` | 46.5vw | 6vh | W4c.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W4c.footer.primary` | 46.5vw | 6vh | W4c.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W4c.footer.singleAction` | 95vw | 6vh | W4c.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |
| `W4c.footer.skipSpeech` | 30.67vw | 6vh | W4c.footer | bottom-center | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Three-action variant: each button takes one third minus shared gaps; replaces two-action widths |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Compact HUD · z6   [Menu] │
│ skybox z2                 │
│  ╭─╮             ╭─╮      │
│  │P│ z4       z4 │N│      │
│ ─┤ ├── floor z3 ┤ ├─      │
│ both always visible       │
│ ═ reveal line · z5 ═══    │
│ Quest title     [Voice Ⅱ] │
│ One short caption beat.   │
│ [Response 1]              │
│ [Response 2]              │
│ [Response 3]              │
│ [Response 4]              │
│ [Back]   [Skip][Continue] │
└───────────────────────────┘
```

**Component IDs and nominal viewport allocations**

| Component / child binding | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Placement / budget |
|---|---:|---:|---|---|---|---|---|---|
| `W4c.frame` | 100vw | 100vh | visible game viewport | center | center / center | root grid item | equal outer margins; safe area applied once | Centered host; W4 fills available game viewport |
| `W4c.header` | 100vw | 10vh | W4c.frame | top / full width | stretch / center | normal grid flow | 0 | Reserved top band |
| `W4c.header.title` | 82vw | 6vh | W4c.header | top-left | start / center | normal grid flow | 2.5vw from left; vertically centered in header | Top-left, shared inset |
| `W4c.header.exit` | 8vw | 6vh | W4c.header | top-right | end / center | normal grid flow | 2.5vw from right; vertically centered in header | Top-right; min-target rule may enlarge |
| `W4c.body` | 100vw | 75vh | W4c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes internal padding; scroll only this region if needed |
| `W4c.scene` | 100vw | 40vh | W4c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4c.context` | 100vw | 35vh | W4c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Includes region internal spacing |
| `W4c.scene.skybox` | 100vw | 26vh | W4c.frame | frame top | stretch / start | layer z2 · absolute sheet, bottom of the stack | 0; runs behind the HUD band | Layer z2 · frame top → floor line; runs behind the HUD band |
| `W4c.scene.floor` | 100vw | 74vh | W4c.frame | floor line | stretch / start | layer z3 · absolute sheet | 0; runs behind the context and footer bands | Layer z3 · floor line at 26vh → frame bottom; runs behind the context and footer bands |
| `W4c.scene.playerPortrait` | 30vw | 114vh figure · 38vh visible | W4c.frame | left slot top | center / start | layer z4 · absolute; overruns the scene into lower layers | 2.5vw from respective frame edge; top 2vh below the scene top | Layer z4 · stays inside its 46.5vw lane; shrinks only when wider than the lane · full figure zoomed so the top 1/3 spans slot top (12vh) → reveal line; the rest is occluded by the context band, footer and frame edge, never cropped |
| `W4c.scene.npcPortrait` | 30vw | 114vh figure · 38vh visible | W4c.frame | right slot top | center / start | layer z4 · absolute; overruns the scene into lower layers | 2.5vw from respective frame edge; top 2vh below the scene top | Layer z4 · as the player, mirrored; lanes are half the frame less insets and a gap of 2vw or 24px, whichever is larger, so both are always visible; a figure wider than its 46.5vw lane shrinks and sinks to the reveal line; the speaker draws above the listener |
| `W4c.context.revealLine` | 100vw | edge at 50vh | W4c.context | context top edge | stretch / start | occluding edge of layer z5 | 0 | Top edge of the opaque context band; everything above it is visible |
| `W4c.context.dialogue` | 95vw | 31vh | W4c.context | center | center / center | inside layer z5 (opaque band) | 2.5vw horizontal / 2vh vertical inset, except map has no vertical inset | Layer z5 · opaque band; quest title, narrative, responses; no sub-headings |
| `W4c.context.questTitle` | 95vw | 1 line | W4c.context.dialogue | top-left | start / start | column row 1 | 2.5vw side inset; half gap to the band top | Replaces the eyebrow, speaker line and prompt hint |
| `W4c.context.narrative` | 95vw | ≤ 2 lines | W4c.context.dialogue | below quest title | start / start | column row 2 | half gap | One authored beat |
| `W4c.context.responses` | 95vw | 4 rows × min target | W4c.context.dialogue | below narrative | stretch / start | response grid; columns from config.context.responseColumns | half gap between rows and columns | 4 responses in 1 column fit without scrolling; the band scrolls only past 4; each response wraps to 2 lines |
| `W4c.footer` | 100vw | 15vh | W4c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Reserved bottom band; no extra height outside total |
| `W4c.footer.closeBack` | 46.5vw | 6vh | W4c.footer | bottom-left | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-left when two actions |
| `W4c.footer.primary` | 46.5vw | 6vh | W4c.footer | bottom-right | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Bottom-right when two actions |
| `W4c.footer.singleAction` | 95vw | 6vh | W4c.footer | bottom / full usable width | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Alternative: exactly one action fills usable width |
| `W4c.footer.skipSpeech` | 30.67vw | 6vh | W4c.footer | bottom-center | center / center | single-row footer grid item | 2.5vw side inset; vertically centered in footer | Three-action variant: each button takes one third minus shared gaps; replaces two-action widths |

Nested rows are subdivisions, not additional viewport bands. Control heights and text are subject to shared readable/minimum-target sizing; figures are proposed targets, not measured pixels. Rendered capability filtering can omit slots.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: dialogueGraph, questSnapshot, playerIdentity, npcIdentity, locale
PARENT: W4; BODY: dialogue replacing hand/cards
scene = config.W4c    // bands, background, layers, layerOrder, portraits, context, entrance
bands = AllocateW4Bands(frame, scene.bands)
revealLine = bands.context.top
floorLine = bands.scene.top + bands.scene.height × GroundLineFraction(scene.background.floorHeightPercent)
FOR layer IN SortByZ(scene.layerOrder):           // bottom of the stack first
    IF scene.layers[layer.id]: PaintLayer(layer, bands)
// skybox: frame top → floorLine, behind every band
// floor: floorLine → frame bottom, behind the context and footer bands
// playerPortrait, npcPortrait: FullFigure(art) placed by
//     CloseUpPlacement(art, Slot(side, scene.portraits.slot), revealLine, scene.portraits.visibleFraction)
//     the opaque context band occludes each figure below revealLine; never crop or mask
//     gap = Max(scene.portraits.minGapVw × frame.width, scene.portraits.minGapPx)   // pixel floor for narrow screens
//     lane.width = (frame.width - config.referenceTokens.value_2.value × slot.inset - gap) / config.referenceTokens.value_2.value   // one lane per side; never overlap
//     IF scene.portraits.fit = shrinkToLane: figure.height = Min(figure.height, lane.width / ArtAspect(art))
//     figure.center = Clamp(slot.center, lane.start + figure.width / config.referenceTokens.value_2.value, lane.end - figure.width / config.referenceTokens.value_2.value)
//     IF scene.portraits.anchor = revealLine: figure.top = revealLine - figure.height × visibleFraction   // never hover
//     the speaker draws above the listener when scene.portraits.speakerAbove; the listener dims to scene.portraits.listener at most
// context: opaque band from revealLine; RenderInOrder(scene.context.order) = quest title, narrative, responses
//     no eyebrow, speaker line or prompt hint (scene.context.headings); the speaker is named under the portrait
//     responses = ResponseGrid(choices, scene.context.responseColumns[mode])
//     IF Count(choices) <= scene.context.maxVisibleResponses: ShrinkToFit(context, noScroll) ELSE ScrollBody(context)
// hud, footer: shared HUD; [Back, SkipSpeech, Continue]
model.actions = [Back, SkipSpeech, Continue]
PlayEntrance(scene.entrance); EnableControlsAfterLastEntranceStep()
KeepFooterInlineAndPortraitSidesStable()

FUNCTION ShowBeat(id, reviewing = false):
    StopClipAndTimers(); generation = NextGenerationToken()
    activeGeneration = generation
    ProjectAndRenderBeat(id)
    IF reviewing OR noPlayableVoice: AwaitManualContinue(); RETURN
    Speech.Play(clip, onEnded = FUNCTION:
        IF disposed OR activeGeneration != generation: RETURN
        IF autoAdvance AND nextBeatIsLinearAndNonCommitting:
            ScheduleConfiguredReadablePauseThenAdvance(activeGeneration)
        ELSE AwaitChoiceOrContinue())

ON Continue: CancelPendingAdvance(); AdvanceOneBeatOrConfirmSelectedChoice()
ON SkipSpeech: CancelPendingAdvance(); MoveToNextRequiredBoundaryWithoutEffects()
ON Back: ReviewPreviousVisitedBeatWithoutUndoingOrReplayingDomainCommands()
ON chooseResponse: RevalidateQuestState(); DispatchConfirmedChoiceOnce()
ON voicePause: PauseClipAndAutoAdvanceTimer()
ON audioFailure/mute: KeepCaptionsAndManualControlsAvailable()
ON dispose: InvalidateGeneration(); StopClipTimersAndListeners()
Audio completion, Back, and Skip never grant rewards or choose responses.
```

## Verification

Verify every parent and child in all three views, at supported text/UI scales, with keyboard/controller/touch, long copy, empty/blocked states, active modal and preserved selection across rotation. Portrait is an explicit target: audit the existing upright/orientation gate before implementation. Do not shrink hit targets or clip required text to claim a no-scroll layout. This is a documentation specification; it does not claim implemented or browser-verified UI.

## Card hierarchy — WC0 and descendants

**WC0 shared geometry:** header 10%, art 40%, body 40%, footer 10% of card height (not viewport height). Art includes meaningful tag badges; body includes rules and blocker. All drawings show selected state: outline follows the lifted card; (i) is centered above it and fades in after 1000ms of continuous selection. Info opens W1w. Card footer contains supplementary metadata; commands belong to the owning host. All cards share aspect ratio 5:8 (proposed token), with clamped width and derived height; viewport equivalents use labeled reference viewports. See CARD-SELECTION-CONTRACT.md.

Every card inherits WC0 bones/effects and adds components through validated tag rules. This is a design/specification artifact, not implemented game code. Proposed tag IDs below are illustrative vocabulary to map against the existing tag registry in the specification task; they are not claimed to exist. Read CARD-CONSTRUCTION-CONTRACT.md before implementation.

Names use `WCid.region.component`; named detail rows include their semantic sub-name in the size table. Width/height values are nominal **vh/vw of the visible game viewport**, not percentages of the card. Reference viewports: wide 1600×1000, compact 1000×800, portrait 400×800; actual runtime height is width / aspectRatio, never independently clamped. Children subdivide their card envelope; do not add their heights to the envelope. Actual cards are hosted in hands/grids/pickers: use container allocation, readable minimums, and explicit overflow/paging rather than shrinking content. See COMPONENT-SIZING.md.


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/screens/map.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/map.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/screens/event.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGS1](wireframe-gallery.html#WGS1), [WGS2](wireframe-gallery.html#WGS2), [WGS3](wireframe-gallery.html#WGS3), [WGS4](wireframe-gallery.html#WGS4), [WGS5](wireframe-gallery.html#WGS5), [WGQ1](wireframe-gallery.html#WGQ1), [WGQ2](wireframe-gallery.html#WGQ2), [WGQ3](wireframe-gallery.html#WGQ3), [WGQ4](wireframe-gallery.html#WGQ4), [WGQ5](wireframe-gallery.html#WGQ5), [WGQ6](wireframe-gallery.html#WGQ6), [WGQ7](wireframe-gallery.html#WGQ7), [WGQ8](wireframe-gallery.html#WGQ8), [WGS6](wireframe-gallery.html#WGS6), [WGS7](wireframe-gallery.html#WGS7), [WGH1](wireframe-gallery.html#WGH1), [WGH2](wireframe-gallery.html#WGH2), [WGH3](wireframe-gallery.html#WGH3), [WGH4](wireframe-gallery.html#WGH4), [WGH5](wireframe-gallery.html#WGH5), [WGH6](wireframe-gallery.html#WGH6), [WGH7](wireframe-gallery.html#WGH7), [WGH8](wireframe-gallery.html#WGH8)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [component-completion-client.js](component-completion-client.js), [hud-reference-client.js](hud-reference-client.js), [scene-reference-client.js](scene-reference-client.js)

Renderer binding: `gameplayPreview`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [component-completion.css](component-completion.css), [scene-reference.css](scene-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "context": "W4c",
  "scene": {
    "parent": "W4",
    "bands": {
      "hud": 10,
      "scene": 40,
      "context": 35,
      "footer": 15
    },
    "layers": {
      "skybox": true,
      "floor": true,
      "playerPortrait": true,
      "npcPortrait": true,
      "context": true,
      "hud": true,
      "footer": true
    },
    "components": [
      "WGS1",
      "WGS2",
      "WGS3",
      "WGS4",
      "WGS5",
      "WGQ1",
      "WGQ2",
      "WGQ3",
      "WGQ4",
      "WGQ5",
      "WGQ6",
      "WGQ7",
      "WGQ8",
      "WGS6",
      "WGS7",
      "WGH1",
      "WGH2",
      "WGH3",
      "WGH4",
      "WGH5",
      "WGH6",
      "WGH7",
      "WGH8"
    ],
    "background": {
      "floorHeightPercent": 60,
      "skylineColor": "#635139",
      "floorColor": "#382c20"
    },
    "hud": {
      "context": "town",
      "enabled": true,
      "layoutPreset": "proposed",
      "layers": {
        "header": true,
        "class": true,
        "cinders": true,
        "position": true,
        "vitality": true,
        "health": true,
        "mana": true,
        "stamina": false,
        "armoury": true,
        "menu": true,
        "rail": false,
        "relics": true,
        "potions": true,
        "experience": true,
        "chargeFlasks": false
      },
      "experience": {
        "contexts": [
          "combat"
        ],
        "color": "#398bd1",
        "heightRem": 0.35,
        "animationMs": 650,
        "awardPreview": 15
      },
      "layout": {
        "gapRem": 0.15,
        "insetRem": 0.15,
        "meterHeightRem": 0.8,
        "actionHeightRem": 2.75,
        "radiusRem": 0.25
      },
      "colors": {
        "background": "#211a12",
        "gold": "#d5af68",
        "text": "#eee2ca",
        "health": "#668c46",
        "mana": "#478dbc",
        "stamina": "#bf9949"
      },
      "sample": {
        "className": "Warden",
        "cinders": 120,
        "act": 1,
        "floor": 4,
        "health": {
          "value": 32,
          "maximum": 40
        },
        "mana": {
          "value": 6,
          "maximum": 10
        },
        "stamina": {
          "value": 8,
          "maximum": 12
        },
        "experience": {
          "value": 40,
          "maximum": 100
        },
        "relics": [
          "Ash seal",
          "Ember charm"
        ],
        "potions": [
          "Smoke vial ×1"
        ],
        "chargeFlasks": [
          "HP ×2",
          "MP ×1"
        ]
      }
    },
    "minimumHudHeightRem": 5.5,
    "layerOrder": [
      {
        "id": "skybox",
        "z": 2,
        "extent": "frame top to floor line, behind every band"
      },
      {
        "id": "floor",
        "z": 3,
        "extent": "floor line to frame bottom, behind the context and footer bands"
      },
      {
        "id": "playerPortrait",
        "z": 4,
        "extent": "slot top to three times the visible band; lower part occluded"
      },
      {
        "id": "npcPortrait",
        "z": 4,
        "extent": "slot top to three times the visible band; lower part occluded"
      },
      {
        "id": "context",
        "z": 5,
        "occludes": true,
        "extent": "reveal line to footer top, opaque"
      },
      {
        "id": "hud",
        "z": 6,
        "extent": "frame top to scene window top"
      },
      {
        "id": "footer",
        "z": 6,
        "extent": "footer top to frame bottom"
      }
    ],
    "portraits": {
      "slot": {
        "widthVw": 20,
        "compactWidthVw": 30,
        "insetVw": 2.5,
        "topOffsetVh": 2
      },
      "visibleFraction": {
        "numerator": 1,
        "denominator": 3
      },
      "speaker": "npcPortrait",
      "speakerAbove": true,
      "mirrorNpc": true,
      "alwaysVisible": true,
      "minGapVw": 2,
      "fit": "shrinkToLane",
      "anchor": "revealLine",
      "listener": {
        "minOpacity": 0.85,
        "brightness": 0.8,
        "saturation": 0.7
      },
      "minGapPx": 24
    },
    "entrance": [
      {
        "layers": [
          "skybox",
          "floor",
          "hud",
          "footer"
        ],
        "atMs": 0,
        "fadeMs": 0
      },
      {
        "layers": [
          "playerPortrait",
          "npcPortrait"
        ],
        "atMs": 0,
        "fadeMs": 400
      },
      {
        "layers": [
          "context"
        ],
        "atMs": 400,
        "fadeMs": 300,
        "riseVh": 2
      }
    ],
    "configSource": "Game values live in content/config/ui/scenes/w4c-dialogue.json; this block mirrors them for the gallery.",
    "context": {
      "headings": {
        "questEyebrow": false,
        "speakerLine": false,
        "promptHint": false
      },
      "order": [
        "questTitle",
        "narrative",
        "responses"
      ],
      "questTitleLines": 1,
      "narrativeMaxLines": 2,
      "maxVisibleResponses": 4,
      "responseColumns": {
        "wide": 2,
        "compact": 2,
        "portrait": 1
      },
      "responseMaxLines": 2,
      "overflow": "the band scrolls only when responses exceed maxVisibleResponses",
      "speakerName": "shown under the speaking portrait, not in the band",
      "titleClearsLineBox": true
    }
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "scene": {
    "parent": "W4",
    "bands": {
      "hud": 10,
      "scene": 40,
      "context": 35,
      "footer": 15
    },
    "layers": {
      "skybox": true,
      "floor": true,
      "playerPortrait": true,
      "npcPortrait": true,
      "context": true,
      "hud": true,
      "footer": true
    },
    "components": [
      "WGS1",
      "WGS2",
      "WGS3",
      "WGS4",
      "WGS5",
      "WGQ1",
      "WGQ2",
      "WGQ3",
      "WGQ4",
      "WGQ5",
      "WGQ6",
      "WGQ7",
      "WGQ8",
      "WGS6",
      "WGS7",
      "WGH1",
      "WGH2",
      "WGH3",
      "WGH4",
      "WGH5",
      "WGH6",
      "WGH7",
      "WGH8"
    ],
    "background": {
      "floorHeightPercent": 60,
      "skylineColor": "#635139",
      "floorColor": "#382c20"
    },
    "hud": {
      "context": "town",
      "enabled": true,
      "layoutPreset": "proposed",
      "layers": {
        "header": true,
        "class": true,
        "cinders": true,
        "position": true,
        "vitality": true,
        "health": true,
        "mana": true,
        "stamina": false,
        "armoury": true,
        "menu": true,
        "rail": false,
        "relics": true,
        "potions": true,
        "experience": true,
        "chargeFlasks": false
      },
      "experience": {
        "contexts": [
          "combat"
        ],
        "color": "#398bd1",
        "heightRem": 0.35,
        "animationMs": 650,
        "awardPreview": 15
      },
      "layout": {
        "gapRem": 0.15,
        "insetRem": 0.15,
        "meterHeightRem": 0.8,
        "actionHeightRem": 2.75,
        "radiusRem": 0.25
      },
      "colors": {
        "background": "#211a12",
        "gold": "#d5af68",
        "text": "#eee2ca",
        "health": "#668c46",
        "mana": "#478dbc",
        "stamina": "#bf9949"
      },
      "sample": {
        "className": "Warden",
        "cinders": 120,
        "act": 1,
        "floor": 4,
        "health": {
          "value": 32,
          "maximum": 40
        },
        "mana": {
          "value": 6,
          "maximum": 10
        },
        "stamina": {
          "value": 8,
          "maximum": 12
        },
        "experience": {
          "value": 40,
          "maximum": 100
        },
        "relics": [
          "Ash seal",
          "Ember charm"
        ],
        "potions": [
          "Smoke vial ×1"
        ],
        "chargeFlasks": [
          "HP ×2",
          "MP ×1"
        ]
      }
    },
    "minimumHudHeightRem": 5.5,
    "layerOrder": [
      {
        "id": "skybox",
        "z": 2,
        "extent": "frame top to floor line, behind every band"
      },
      {
        "id": "floor",
        "z": 3,
        "extent": "floor line to frame bottom, behind the context and footer bands"
      },
      {
        "id": "playerPortrait",
        "z": 4,
        "extent": "slot top to three times the visible band; lower part occluded"
      },
      {
        "id": "npcPortrait",
        "z": 4,
        "extent": "slot top to three times the visible band; lower part occluded"
      },
      {
        "id": "context",
        "z": 5,
        "occludes": true,
        "extent": "reveal line to footer top, opaque"
      },
      {
        "id": "hud",
        "z": 6,
        "extent": "frame top to scene window top"
      },
      {
        "id": "footer",
        "z": 6,
        "extent": "footer top to frame bottom"
      }
    ],
    "portraits": {
      "slot": {
        "widthVw": 20,
        "compactWidthVw": 30,
        "insetVw": 2.5,
        "topOffsetVh": 2
      },
      "visibleFraction": {
        "numerator": 1,
        "denominator": 3
      },
      "speaker": "npcPortrait",
      "speakerAbove": true,
      "mirrorNpc": true,
      "alwaysVisible": true,
      "minGapVw": 2,
      "fit": "shrinkToLane",
      "anchor": "revealLine",
      "listener": {
        "minOpacity": 0.85,
        "brightness": 0.8,
        "saturation": 0.7
      },
      "minGapPx": 24
    },
    "entrance": [
      {
        "layers": [
          "skybox",
          "floor",
          "hud",
          "footer"
        ],
        "atMs": 0,
        "fadeMs": 0
      },
      {
        "layers": [
          "playerPortrait",
          "npcPortrait"
        ],
        "atMs": 0,
        "fadeMs": 400
      },
      {
        "layers": [
          "context"
        ],
        "atMs": 400,
        "fadeMs": 300,
        "riseVh": 2
      }
    ],
    "configSource": "Game values live in content/config/ui/scenes/w4c-dialogue.json; this block mirrors them for the gallery.",
    "context": {
      "headings": {
        "questEyebrow": false,
        "speakerLine": false,
        "promptHint": false
      },
      "order": [
        "questTitle",
        "narrative",
        "responses"
      ],
      "questTitleLines": 1,
      "narrativeMaxLines": 2,
      "maxVisibleResponses": 4,
      "responseColumns": {
        "wide": 2,
        "compact": 2,
        "portrait": 1
      },
      "responseMaxLines": 2,
      "overflow": "the band scrolls only when responses exceed maxVisibleResponses",
      "speakerName": "shown under the speaking portrait, not in the band",
      "titleClearsLineBox": true
    }
  },
  "hud": {
    "context": "combat",
    "enabled": true,
    "layoutPreset": "proposed",
    "layers": {
      "header": true,
      "class": true,
      "cinders": true,
      "position": true,
      "vitality": true,
      "health": true,
      "mana": true,
      "stamina": true,
      "armoury": true,
      "menu": true,
      "rail": true,
      "relics": true,
      "potions": true,
      "experience": true,
      "chargeFlasks": true
    },
    "vitality": {
      "referenceMaximum": {
        "health": 200,
        "mana": 20,
        "stamina": 20
      },
      "maximumWidthPercent": 100,
      "scaleByMaximum": true
    },
    "potions": {
      "placement": "footerOnly",
      "componentId": "WGC11",
      "contentsComponentId": "WGH8",
      "openIntent": "openPotions",
      "combineChargeFlasks": true,
      "combineCarriedPotions": true
    },
    "experience": {
      "contexts": [
        "combat"
      ],
      "color": "#398bd1",
      "heightRem": 0.35,
      "animationMs": 650,
      "awardPreview": 15
    },
    "phone": {
      "gate": "data-layout='narrow'",
      "maxWidthPx": 520,
      "meters": "stacked",
      "layerOverrides": {
        "class": false,
        "position": false,
        "route": false
      }
    },
    "diagram": {
      "columns": {
        "wide": 62,
        "compact": 48,
        "portraitSE": 40,
        "portraitS24": 42
      },
      "trackColumns": 20,
      "labelColumns": 8
    },
    "layout": {
      "gapRem": 0.35,
      "insetRem": 0.5,
      "meterHeightRem": 1.15,
      "actionHeightRem": 2.75,
      "radiusRem": 0.25
    },
    "colors": {
      "background": "#211a12",
      "gold": "#d5af68",
      "text": "#eee2ca",
      "health": "#668c46",
      "mana": "#478dbc",
      "stamina": "#bf9949"
    },
    "sample": {
      "className": "Warden",
      "cinders": 120,
      "act": 1,
      "floor": 4,
      "health": {
        "value": 32,
        "maximum": 40
      },
      "mana": {
        "value": 6,
        "maximum": 10
      },
      "stamina": {
        "value": 8,
        "maximum": 10
      },
      "experience": {
        "value": 40,
        "maximum": 100
      },
      "relics": [
        "Ash seal",
        "Ember charm"
      ],
      "potions": [
        "Smoke vial ×1"
      ],
      "chargeFlasks": [
        "HP ×2",
        "MP ×1"
      ]
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WC0: Master card

**Parent: none — master card.** Master preview selection reveals only the shared info control; no Select or Eligible target demo buttons. All structural and optional components are resolved by validated tag rules. No label-based construction.

**Construction tags (proposed):** `presentable:card`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ {Registered content slots}     │
│ {Availability / reason}        │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC0.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC0.header | 100% card width | config.cards.geometry.bands.header% of card height | WC0.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC0.header.title | remaining header width | 100% header height | WC0.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC0.header.state | content-fit | 100% header height | WC0.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC0.art | 100% card width | config.cards.geometry.bands.art% of card height | WC0.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC0.art.tags | available art width minus shared inset | content-fit within art band | WC0.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC0.body | 100% card width | config.cards.geometry.bands.body% of card height | WC0.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC0.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC0.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC0.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC0.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC0.body.detail1 | 100% usable body width | content-fit within body band | WC0.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | {Registered content slots} |
| WC0.body.detail2 | 100% usable body width | content-fit within body band | WC0.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | {Availability / reason} |
| WC0.body.blocker | 100% usable body width | content-fit within body band | WC0.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC0.selection.outline | 100% card width | 100% card height | WC0.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC0.selection.info | config.components.target.minRem | config.components.target.minRem | WC0.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ {Registered content slots} │
│ {Availability / reason}    │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC0.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC0.header | 100% card width | config.cards.geometry.bands.header% of card height | WC0.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC0.header.title | remaining header width | 100% header height | WC0.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC0.header.state | content-fit | 100% header height | WC0.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC0.art | 100% card width | config.cards.geometry.bands.art% of card height | WC0.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC0.art.tags | available art width minus shared inset | content-fit within art band | WC0.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC0.body | 100% card width | config.cards.geometry.bands.body% of card height | WC0.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC0.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC0.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC0.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC0.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC0.body.detail1 | 100% usable body width | content-fit within body band | WC0.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | {Registered content slots} |
| WC0.body.detail2 | 100% usable body width | content-fit within body band | WC0.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | {Availability / reason} |
| WC0.body.blocker | 100% usable body width | content-fit within body band | WC0.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC0.selection.outline | 100% card width | 100% card height | WC0.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC0.selection.info | config.components.target.minRem | config.components.target.minRem | WC0.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ {Registered content      │
│ slots}                   │
│ {Availability / reason}  │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC0.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC0.header | 100% card width | config.cards.geometry.bands.header% of card height | WC0.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC0.header.title | remaining header width | 100% header height | WC0.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC0.header.state | content-fit | 100% header height | WC0.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC0.art | 100% card width | config.cards.geometry.bands.art% of card height | WC0.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC0.art.tags | available art width minus shared inset | content-fit within art band | WC0.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC0.body | 100% card width | config.cards.geometry.bands.body% of card height | WC0.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC0.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC0.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC0.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC0.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC0.body.detail1 | 100% usable body width | content-fit within body band | WC0.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | {Registered content slots} |
| WC0.body.detail2 | 100% usable body width | content-fit within body band | WC0.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | {Availability / reason} |
| WC0.body.blocker | 100% usable body width | content-fit within body band | WC0.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC0.selection.outline | 100% card width | 100% card height | WC0.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC0.selection.info | config.components.target.minRem | config.components.target.minRem | WC0.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ {Registered content      │
│ slots}                   │
│ {Availability / reason}  │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC0.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC0.header | 100% card width | config.cards.geometry.bands.header% of card height | WC0.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC0.header.title | remaining header width | 100% header height | WC0.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC0.header.state | content-fit | 100% header height | WC0.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC0.art | 100% card width | config.cards.geometry.bands.art% of card height | WC0.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC0.art.tags | available art width minus shared inset | content-fit within art band | WC0.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC0.body | 100% card width | config.cards.geometry.bands.body% of card height | WC0.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC0.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC0.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC0.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC0.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC0.body.detail1 | 100% usable body width | content-fit within body band | WC0.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | {Registered content slots} |
| WC0.body.detail2 | 100% usable body width | content-fit within body band | WC0.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | {Availability / reason} |
| WC0.body.blocker | 100% usable body width | content-fit within body band | WC0.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC0.selection.outline | 100% card width | 100% card height | WC0.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC0.selection.info | config.components.target.minRem | config.components.target.minRem | WC0.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context, validatedTagRegistry
OUTPUT: immutable CardViewModel + semantic action intents

FUNCTION ConstructCard(input):
    definition = registries.requireEntity(entityRef)
    authoredTags = tagService.tagsForValidatedIdentity(entityRef)
    stateFacts = domainQueries.projectCurrentFacts(instanceSnapshot, context)
    effectiveTags = registeredTagRules.derive(authoredTags, stateFacts)
    matchedRules = MatchAllAnyExcludedTagRelations(effectiveTags, context)
    ancestry = ResolveDeclaredCardFamilyFromMatchedRules()
    REQUIRE ancestry terminates at WC0; reject cycles/unknown families
    components = ResolveSlotRulesByPriorityAndExplicitCompatibility(matchedRules)
    REQUIRE required identity slots exist
    REQUIRE no unresolved exclusive-slot collision
    providers = ResolveAllowlistedProviders(components)
    model = ProjectImmutableValues(providers, definition, stateFacts)
    // Read the authoritative cost projection; variable action cost uses the configured X label.
    model.costs = ProjectRegisteredCostProfile(stateFacts, definition)
    costRows = FilterAndOrderProjectedCosts(model.costs, config.cards.costs.order, config.cards.costs.providers)
    RenderCompactLeftCostRail(costRows, config.cards.costs, top=ResolveHeaderBandHeight(config.cards.geometry) + ResolveInset(config.cards.costs.insetRem)) // Anchor below header inside art; render outlined icon and number only, no box/background.
    model.actions = DomainAvailableCommands(context, entityRef) // owning host only
    RETURN model with named slots and semantic actions

FUNCTION RenderCard(model):
    ResolveInheritedCardTokensAndHostSize(model, mode)
    FOR each ordered slot IN model.components:
        RenderRegisteredComponent(slot.componentId, slot.displayData)
    BindSelectionSeparateFromExplicitPlayUseEquipCommands()
    ApplyCardRelativeBands(config.card.bandFractions)
    // Visible badges are inside art; outline and info are outside band budget.
    ApplySharedPaletteFocusSelectionAndDisabledStates()
    RenderFooterMetadataOnly(); owningHost.RendersAvailableActionOutsideCard()
    // No Use/Play/Equip command belongs inside the card footer.

ON action:
    EmitIntentToOwningPresenter(); domain command revalidates current state
ON selectionChanged(selected):
    CancelPendingInfoTimerAndFade(); generation = NextSelectionGeneration()
    IF selected:
        ApplySharedSelectionOutline(); LiftVisuallyWithoutReflow()
        IF cardKind is not combatant: owningHost.ShowApplicableContextAction(); HighlightDomainEligibleTargets()
        DisableCommitUntilRequiredTargetIsSelected()
        After(config.infoDelayMs = config.referenceTokens.value_1000.value):
            IF stillSelected AND mounted AND generationIsCurrent:
                FadeInInfoButton(config.infoFadeMs); EnableInfoInput()
    ELSE: RemoveLiftOutlineAndInfo(); RestoreNormalStackOrder()
        owningHost.ClearContextActionAndTargetHighlights()
ON infoActivated:
    StopPropagation(); CancelPendingInfoTimerAndFade()
    OpenW1wCardInspector(entityRef, context, returnFocusTarget)
    // Never play/use/equip the card from this event.
ON inspectShortcut: OpenW1wCardInspectorImmediately()
ON inspectorClosed: RestoreSelectionAndFocusIfEntityStillExists()
ON inspect:
    OpenW1wCardInspectorWithoutMutatingEntity()
ON update/dispose:
    PreserveStableIdentityAndFocus(); release listeners/tooltips
Never branch on entity names or inject executable markup from tags.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC0",
  "tags": [
    "presentable:card"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": null,
    "name": "Reference card",
    "kind": "card",
    "glyph": "◇",
    "headerState": "Known",
    "rules": "A shared card face populated by its view model.",
    "facts": [
      [
        "Type",
        "Reference"
      ]
    ],
    "rarity": "Common",
    "owned": 1,
    "tags": [],
    "construction": [
      "feature:rules",
      "feature:facts"
    ],
    "availability": null,
    "flavor": "An illustrative card.",
    "references": [
      "src/ui/components/card.js",
      "src/ui/components/cardInspection.js"
    ]
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC1: Playing card

**Parent: WC0.** Inherits card identity/art/tags; adds a compact left-edge cost stack below the header inside the art containing every projected action/stamina/MP cost, followed by targeting and effect components. Costs come from playingCardModel.costs in src/model/playingCard.js; renderer does not calculate or invent costs.

**Construction tags (proposed):** `card-kind:playing`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ ◆ x   [Art / portrait]         │
│ ϟ x   optional stamina         │
│ ♢ x   optional MP              │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Targeting                      │
│ Effects / rules                │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1.header.title | remaining header width | 100% header height | WC1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1.header.state | content-fit | 100% header height | WC1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1.art.tags | available art width minus shared inset | content-fit within art band | WC1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1.body.detail1 | 100% usable body width | content-fit within body band | WC1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Targeting |
| WC1.body.detail2 | 100% usable body width | content-fit within body band | WC1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Effects / rules |
| WC1.body.blocker | 100% usable body width | content-fit within body band | WC1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1.selection.outline | 100% card width | 100% card height | WC1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1.selection.info | config.components.target.minRem | config.components.target.minRem | WC1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ ◆ x   [Art / portrait]     │
│ ϟ x   optional stamina     │
│ ♢ x   optional MP          │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Targeting                  │
│ Effects / rules            │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1.header.title | remaining header width | 100% header height | WC1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1.header.state | content-fit | 100% header height | WC1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1.art.tags | available art width minus shared inset | content-fit within art band | WC1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1.body.detail1 | 100% usable body width | content-fit within body band | WC1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Targeting |
| WC1.body.detail2 | 100% usable body width | content-fit within body band | WC1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Effects / rules |
| WC1.body.blocker | 100% usable body width | content-fit within body band | WC1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1.selection.outline | 100% card width | 100% card height | WC1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1.selection.info | config.components.target.minRem | config.components.target.minRem | WC1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ ◆ x   [Art / portrait]   │
│ ϟ x   optional stamina   │
│ ♢ x   optional MP        │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Targeting                │
│ Effects / rules          │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1.header.title | remaining header width | 100% header height | WC1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1.header.state | content-fit | 100% header height | WC1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1.art.tags | available art width minus shared inset | content-fit within art band | WC1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1.body.detail1 | 100% usable body width | content-fit within body band | WC1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Targeting |
| WC1.body.detail2 | 100% usable body width | content-fit within body band | WC1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Effects / rules |
| WC1.body.blocker | 100% usable body width | content-fit within body band | WC1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1.selection.outline | 100% card width | 100% card height | WC1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1.selection.info | config.components.target.minRem | config.components.target.minRem | WC1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ ◆ x   [Art / portrait]   │
│ ϟ x   optional stamina   │
│ ♢ x   optional MP        │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Targeting                │
│ Effects / rules          │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1.header.title | remaining header width | 100% header height | WC1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1.header.state | content-fit | 100% header height | WC1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1.art.tags | available art width minus shared inset | content-fit within art band | WC1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1.body.detail1 | 100% usable body width | content-fit within body band | WC1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Targeting |
| WC1.body.detail2 | 100% usable body width | content-fit within body band | WC1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Effects / rules |
| WC1.body.blocker | 100% usable body width | content-fit within body band | WC1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1.selection.outline | 100% card width | 100% card height | WC1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1.selection.info | config.components.target.minRem | config.components.target.minRem | WC1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC0
REQUIRED PROPOSED TAGS: card-kind:playing
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC1
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC1.body.detail1 ← registered provider for Targeting
    WC1.body.detail2 ← registered provider for Effects / rules
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Select / Play") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Inherits card identity/art/tags; adds a compact left-edge cost stack below the header inside the art containing every projected action/stamina/MP cost, followed by targeting and effect components. Costs come from playingCardModel.costs in src/model/playingCard.js; renderer does not calculate or invent costs.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC1",
  "tags": [
    "card-kind:playing"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC0",
    "name": "Measured Strike",
    "kind": "playing",
    "glyph": "⚔",
    "headerState": "",
    "costs": {
      "action": 1,
      "stamina": 0,
      "mana": 0,
      "variable": false
    },
    "rules": "Deal 8 damage to one eligible enemy.",
    "facts": [
      [
        "Target",
        "One enemy"
      ]
    ],
    "tags": [
      "attack"
    ],
    "construction": [
      "feature:rules",
      "feature:facts",
      "feature:availability"
    ],
    "availability": "Playable when an eligible target exists.",
    "references": [
      "src/model/playingCard.js",
      "src/ui/components/card.js"
    ]
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC1a: Attack card

**Parent: WC1.** Use engine damage preview, including current modifiers; never calculate damage in renderer.

**Construction tags (proposed):** `ability:attack`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ ◆ x   [Art / portrait]         │
│ ϟ x   optional stamina         │
│ ♢ x   optional MP              │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Damage / affected stat         │
│ Target and effect preview      │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1a.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1a.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1a.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1a.header.title | remaining header width | 100% header height | WC1a.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1a.header.state | content-fit | 100% header height | WC1a.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1a.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1a.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1a.art.tags | available art width minus shared inset | content-fit within art band | WC1a.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1a.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1a.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1a.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1a.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1a.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1a.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1a.body.detail1 | 100% usable body width | content-fit within body band | WC1a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Damage / affected stat |
| WC1a.body.detail2 | 100% usable body width | content-fit within body band | WC1a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Target and effect preview |
| WC1a.body.blocker | 100% usable body width | content-fit within body band | WC1a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1a.selection.outline | 100% card width | 100% card height | WC1a.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1a.selection.info | config.components.target.minRem | config.components.target.minRem | WC1a.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ ◆ x   [Art / portrait]     │
│ ϟ x   optional stamina     │
│ ♢ x   optional MP          │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Damage / affected stat     │
│ Target and effect preview  │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1a.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1a.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1a.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1a.header.title | remaining header width | 100% header height | WC1a.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1a.header.state | content-fit | 100% header height | WC1a.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1a.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1a.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1a.art.tags | available art width minus shared inset | content-fit within art band | WC1a.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1a.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1a.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1a.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1a.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1a.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1a.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1a.body.detail1 | 100% usable body width | content-fit within body band | WC1a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Damage / affected stat |
| WC1a.body.detail2 | 100% usable body width | content-fit within body band | WC1a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Target and effect preview |
| WC1a.body.blocker | 100% usable body width | content-fit within body band | WC1a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1a.selection.outline | 100% card width | 100% card height | WC1a.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1a.selection.info | config.components.target.minRem | config.components.target.minRem | WC1a.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ ◆ x   [Art / portrait]   │
│ ϟ x   optional stamina   │
│ ♢ x   optional MP        │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Damage / affected stat   │
│ Target and effect        │
│ preview                  │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1a.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1a.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1a.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1a.header.title | remaining header width | 100% header height | WC1a.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1a.header.state | content-fit | 100% header height | WC1a.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1a.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1a.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1a.art.tags | available art width minus shared inset | content-fit within art band | WC1a.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1a.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1a.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1a.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1a.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1a.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1a.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1a.body.detail1 | 100% usable body width | content-fit within body band | WC1a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Damage / affected stat |
| WC1a.body.detail2 | 100% usable body width | content-fit within body band | WC1a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Target and effect preview |
| WC1a.body.blocker | 100% usable body width | content-fit within body band | WC1a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1a.selection.outline | 100% card width | 100% card height | WC1a.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1a.selection.info | config.components.target.minRem | config.components.target.minRem | WC1a.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ ◆ x   [Art / portrait]   │
│ ϟ x   optional stamina   │
│ ♢ x   optional MP        │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Damage / affected stat   │
│ Target and effect        │
│ preview                  │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1a.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1a.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1a.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1a.header.title | remaining header width | 100% header height | WC1a.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1a.header.state | content-fit | 100% header height | WC1a.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1a.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1a.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1a.art.tags | available art width minus shared inset | content-fit within art band | WC1a.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1a.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1a.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1a.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1a.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1a.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1a.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1a.body.detail1 | 100% usable body width | content-fit within body band | WC1a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Damage / affected stat |
| WC1a.body.detail2 | 100% usable body width | content-fit within body band | WC1a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Target and effect preview |
| WC1a.body.blocker | 100% usable body width | content-fit within body band | WC1a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1a.selection.outline | 100% card width | 100% card height | WC1a.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1a.selection.info | config.components.target.minRem | config.components.target.minRem | WC1a.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC1
REQUIRED PROPOSED TAGS: ability:attack
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC1a
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC1a.body.detail1 ← registered provider for Damage / affected stat
    WC1a.body.detail2 ← registered provider for Target and effect preview
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Select / Play") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Use engine damage preview, including current modifiers; never calculate damage in renderer.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC1a",
  "tags": [
    "ability:attack"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC1",
    "name": "Measured Strike"
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC1b: Skill card

**Parent: WC1.** The effect list is projected from the existing opcode/formula engine.

**Construction tags (proposed):** `ability:skill`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ ◆ x   [Art / portrait]         │
│ ϟ x   optional stamina         │
│ ♢ x   optional MP              │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Defense / utility effects      │
│ Target / requirements          │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1b.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1b.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1b.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1b.header.title | remaining header width | 100% header height | WC1b.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1b.header.state | content-fit | 100% header height | WC1b.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1b.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1b.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1b.art.tags | available art width minus shared inset | content-fit within art band | WC1b.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1b.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1b.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1b.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1b.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1b.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1b.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1b.body.detail1 | 100% usable body width | content-fit within body band | WC1b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Defense / utility effects |
| WC1b.body.detail2 | 100% usable body width | content-fit within body band | WC1b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Target / requirements |
| WC1b.body.blocker | 100% usable body width | content-fit within body band | WC1b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1b.selection.outline | 100% card width | 100% card height | WC1b.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1b.selection.info | config.components.target.minRem | config.components.target.minRem | WC1b.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ ◆ x   [Art / portrait]     │
│ ϟ x   optional stamina     │
│ ♢ x   optional MP          │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Defense / utility effects  │
│ Target / requirements      │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1b.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1b.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1b.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1b.header.title | remaining header width | 100% header height | WC1b.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1b.header.state | content-fit | 100% header height | WC1b.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1b.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1b.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1b.art.tags | available art width minus shared inset | content-fit within art band | WC1b.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1b.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1b.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1b.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1b.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1b.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1b.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1b.body.detail1 | 100% usable body width | content-fit within body band | WC1b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Defense / utility effects |
| WC1b.body.detail2 | 100% usable body width | content-fit within body band | WC1b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Target / requirements |
| WC1b.body.blocker | 100% usable body width | content-fit within body band | WC1b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1b.selection.outline | 100% card width | 100% card height | WC1b.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1b.selection.info | config.components.target.minRem | config.components.target.minRem | WC1b.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ ◆ x   [Art / portrait]   │
│ ϟ x   optional stamina   │
│ ♢ x   optional MP        │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Defense / utility        │
│ effects                  │
│ Target / requirements    │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1b.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1b.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1b.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1b.header.title | remaining header width | 100% header height | WC1b.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1b.header.state | content-fit | 100% header height | WC1b.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1b.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1b.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1b.art.tags | available art width minus shared inset | content-fit within art band | WC1b.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1b.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1b.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1b.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1b.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1b.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1b.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1b.body.detail1 | 100% usable body width | content-fit within body band | WC1b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Defense / utility effects |
| WC1b.body.detail2 | 100% usable body width | content-fit within body band | WC1b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Target / requirements |
| WC1b.body.blocker | 100% usable body width | content-fit within body band | WC1b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1b.selection.outline | 100% card width | 100% card height | WC1b.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1b.selection.info | config.components.target.minRem | config.components.target.minRem | WC1b.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ ◆ x   [Art / portrait]   │
│ ϟ x   optional stamina   │
│ ♢ x   optional MP        │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Defense / utility        │
│ effects                  │
│ Target / requirements    │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1b.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1b.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1b.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1b.header.title | remaining header width | 100% header height | WC1b.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1b.header.state | content-fit | 100% header height | WC1b.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1b.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1b.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1b.art.tags | available art width minus shared inset | content-fit within art band | WC1b.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1b.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1b.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1b.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1b.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1b.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1b.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1b.body.detail1 | 100% usable body width | content-fit within body band | WC1b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Defense / utility effects |
| WC1b.body.detail2 | 100% usable body width | content-fit within body band | WC1b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Target / requirements |
| WC1b.body.blocker | 100% usable body width | content-fit within body band | WC1b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1b.selection.outline | 100% card width | 100% card height | WC1b.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1b.selection.info | config.components.target.minRem | config.components.target.minRem | WC1b.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC1
REQUIRED PROPOSED TAGS: ability:skill
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC1b
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC1b.body.detail1 ← registered provider for Defense / utility effects
    WC1b.body.detail2 ← registered provider for Target / requirements
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Select / Play") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
The effect list is projected from the existing opcode/formula engine.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC1b",
  "tags": [
    "ability:skill"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC1",
    "name": "Brace",
    "glyph": "⛨",
    "rules": "Gain 6 defense.",
    "facts": [
      [
        "Target",
        "Self"
      ]
    ],
    "tags": [
      "skill"
    ],
    "availability": "Playable while the actor can act."
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC1c: Power card

**Parent: WC1.** Persistent rules shown through registered trigger/duration components.

**Construction tags (proposed):** `ability:power`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ ◆ x   [Art / portrait]         │
│ ϟ x   optional stamina         │
│ ♢ x   optional MP              │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Persistent effect              │
│ Trigger / duration             │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1c.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1c.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1c.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1c.header.title | remaining header width | 100% header height | WC1c.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1c.header.state | content-fit | 100% header height | WC1c.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1c.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1c.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1c.art.tags | available art width minus shared inset | content-fit within art band | WC1c.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1c.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1c.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1c.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1c.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1c.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1c.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1c.body.detail1 | 100% usable body width | content-fit within body band | WC1c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Persistent effect |
| WC1c.body.detail2 | 100% usable body width | content-fit within body band | WC1c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Trigger / duration |
| WC1c.body.blocker | 100% usable body width | content-fit within body band | WC1c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1c.selection.outline | 100% card width | 100% card height | WC1c.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1c.selection.info | config.components.target.minRem | config.components.target.minRem | WC1c.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ ◆ x   [Art / portrait]     │
│ ϟ x   optional stamina     │
│ ♢ x   optional MP          │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Persistent effect          │
│ Trigger / duration         │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1c.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1c.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1c.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1c.header.title | remaining header width | 100% header height | WC1c.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1c.header.state | content-fit | 100% header height | WC1c.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1c.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1c.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1c.art.tags | available art width minus shared inset | content-fit within art band | WC1c.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1c.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1c.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1c.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1c.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1c.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1c.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1c.body.detail1 | 100% usable body width | content-fit within body band | WC1c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Persistent effect |
| WC1c.body.detail2 | 100% usable body width | content-fit within body band | WC1c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Trigger / duration |
| WC1c.body.blocker | 100% usable body width | content-fit within body band | WC1c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1c.selection.outline | 100% card width | 100% card height | WC1c.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1c.selection.info | config.components.target.minRem | config.components.target.minRem | WC1c.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ ◆ x   [Art / portrait]   │
│ ϟ x   optional stamina   │
│ ♢ x   optional MP        │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Persistent effect        │
│ Trigger / duration       │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1c.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1c.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1c.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1c.header.title | remaining header width | 100% header height | WC1c.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1c.header.state | content-fit | 100% header height | WC1c.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1c.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1c.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1c.art.tags | available art width minus shared inset | content-fit within art band | WC1c.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1c.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1c.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1c.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1c.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1c.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1c.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1c.body.detail1 | 100% usable body width | content-fit within body band | WC1c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Persistent effect |
| WC1c.body.detail2 | 100% usable body width | content-fit within body band | WC1c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Trigger / duration |
| WC1c.body.blocker | 100% usable body width | content-fit within body band | WC1c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1c.selection.outline | 100% card width | 100% card height | WC1c.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1c.selection.info | config.components.target.minRem | config.components.target.minRem | WC1c.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ ◆ x   [Art / portrait]   │
│ ϟ x   optional stamina   │
│ ♢ x   optional MP        │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Persistent effect        │
│ Trigger / duration       │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1c.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1c.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1c.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1c.header.title | remaining header width | 100% header height | WC1c.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1c.header.state | content-fit | 100% header height | WC1c.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1c.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1c.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1c.art.tags | available art width minus shared inset | content-fit within art band | WC1c.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1c.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1c.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1c.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1c.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1c.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1c.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1c.body.detail1 | 100% usable body width | content-fit within body band | WC1c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Persistent effect |
| WC1c.body.detail2 | 100% usable body width | content-fit within body band | WC1c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Trigger / duration |
| WC1c.body.blocker | 100% usable body width | content-fit within body band | WC1c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1c.selection.outline | 100% card width | 100% card height | WC1c.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1c.selection.info | config.components.target.minRem | config.components.target.minRem | WC1c.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC1
REQUIRED PROPOSED TAGS: ability:power
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC1c
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC1c.body.detail1 ← registered provider for Persistent effect
    WC1c.body.detail2 ← registered provider for Trigger / duration
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Select / Play") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Persistent rules shown through registered trigger/duration components.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC1c",
  "tags": [
    "ability:power"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC1",
    "name": "Ember Focus",
    "glyph": "✦",
    "rules": "Your next attack gains its projected bonus.",
    "facts": [
      [
        "Duration",
        "Until consumed"
      ]
    ],
    "tags": [
      "power"
    ],
    "availability": "Playable while the actor can act."
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC1d: Curse card

**Parent: WC1.** Do not assume all curses are unplayable; preserve the domain rule and context-specific action.

**Construction tags (proposed):** `ability:curse`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ ◆ x   [Art / portrait]         │
│ ϟ x   optional stamina         │
│ ♢ x   optional MP              │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Penalty / consequence          │
│ Playability / removal rule     │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1d.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1d.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1d.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1d.header.title | remaining header width | 100% header height | WC1d.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1d.header.state | content-fit | 100% header height | WC1d.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1d.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1d.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1d.art.tags | available art width minus shared inset | content-fit within art band | WC1d.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1d.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1d.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1d.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1d.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1d.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1d.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1d.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1d.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1d.body.detail1 | 100% usable body width | content-fit within body band | WC1d.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Penalty / consequence |
| WC1d.body.detail2 | 100% usable body width | content-fit within body band | WC1d.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Playability / removal rule |
| WC1d.body.blocker | 100% usable body width | content-fit within body band | WC1d.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1d.selection.outline | 100% card width | 100% card height | WC1d.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1d.selection.info | config.components.target.minRem | config.components.target.minRem | WC1d.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ ◆ x   [Art / portrait]     │
│ ϟ x   optional stamina     │
│ ♢ x   optional MP          │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Penalty / consequence      │
│ Playability / removal rule │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1d.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1d.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1d.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1d.header.title | remaining header width | 100% header height | WC1d.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1d.header.state | content-fit | 100% header height | WC1d.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1d.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1d.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1d.art.tags | available art width minus shared inset | content-fit within art band | WC1d.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1d.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1d.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1d.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1d.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1d.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1d.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1d.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1d.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1d.body.detail1 | 100% usable body width | content-fit within body band | WC1d.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Penalty / consequence |
| WC1d.body.detail2 | 100% usable body width | content-fit within body band | WC1d.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Playability / removal rule |
| WC1d.body.blocker | 100% usable body width | content-fit within body band | WC1d.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1d.selection.outline | 100% card width | 100% card height | WC1d.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1d.selection.info | config.components.target.minRem | config.components.target.minRem | WC1d.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ ◆ x   [Art / portrait]   │
│ ϟ x   optional stamina   │
│ ♢ x   optional MP        │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Penalty / consequence    │
│ Playability / removal    │
│ rule                     │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1d.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1d.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1d.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1d.header.title | remaining header width | 100% header height | WC1d.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1d.header.state | content-fit | 100% header height | WC1d.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1d.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1d.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1d.art.tags | available art width minus shared inset | content-fit within art band | WC1d.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1d.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1d.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1d.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1d.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1d.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1d.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1d.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1d.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1d.body.detail1 | 100% usable body width | content-fit within body band | WC1d.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Penalty / consequence |
| WC1d.body.detail2 | 100% usable body width | content-fit within body band | WC1d.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Playability / removal rule |
| WC1d.body.blocker | 100% usable body width | content-fit within body band | WC1d.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1d.selection.outline | 100% card width | 100% card height | WC1d.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1d.selection.info | config.components.target.minRem | config.components.target.minRem | WC1d.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ ◆ x   [Art / portrait]   │
│ ϟ x   optional stamina   │
│ ♢ x   optional MP        │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Penalty / consequence    │
│ Playability / removal    │
│ rule                     │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1d.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1d.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1d.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1d.header.title | remaining header width | 100% header height | WC1d.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1d.header.state | content-fit | 100% header height | WC1d.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1d.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1d.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1d.art.tags | available art width minus shared inset | content-fit within art band | WC1d.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1d.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1d.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1d.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1d.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1d.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1d.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1d.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1d.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1d.body.detail1 | 100% usable body width | content-fit within body band | WC1d.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Penalty / consequence |
| WC1d.body.detail2 | 100% usable body width | content-fit within body band | WC1d.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Playability / removal rule |
| WC1d.body.blocker | 100% usable body width | content-fit within body band | WC1d.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1d.selection.outline | 100% card width | 100% card height | WC1d.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1d.selection.info | config.components.target.minRem | config.components.target.minRem | WC1d.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC1
REQUIRED PROPOSED TAGS: ability:curse
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC1d
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC1d.body.detail1 ← registered provider for Penalty / consequence
    WC1d.body.detail2 ← registered provider for Playability / removal rule
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Inspect") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Do not assume all curses are unplayable; preserve the domain rule and context-specific action.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC1d",
  "tags": [
    "ability:curse"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC1",
    "name": "Lingering Doubt",
    "glyph": "☾",
    "headerState": "Unplayable",
    "rules": "Carries an unfavorable authored effect.",
    "facts": [
      [
        "Playability",
        "Cannot be played"
      ]
    ],
    "tags": [
      "curse"
    ],
    "availability": "No play action."
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC1e: Status card

**Parent: WC1.** This is a playing-card subtype, distinct from a combat status indicator.

**Construction tags (proposed):** `ability:status`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ ◆ x   [Art / portrait]         │
│ ϟ x   optional stamina         │
│ ♢ x   optional MP              │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Status effect / duration       │
│ Playability / removal rule     │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1e.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1e.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1e.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1e.header.title | remaining header width | 100% header height | WC1e.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1e.header.state | content-fit | 100% header height | WC1e.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1e.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1e.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1e.art.tags | available art width minus shared inset | content-fit within art band | WC1e.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1e.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1e.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1e.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1e.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1e.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1e.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1e.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1e.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1e.body.detail1 | 100% usable body width | content-fit within body band | WC1e.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Status effect / duration |
| WC1e.body.detail2 | 100% usable body width | content-fit within body band | WC1e.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Playability / removal rule |
| WC1e.body.blocker | 100% usable body width | content-fit within body band | WC1e.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1e.selection.outline | 100% card width | 100% card height | WC1e.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1e.selection.info | config.components.target.minRem | config.components.target.minRem | WC1e.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ ◆ x   [Art / portrait]     │
│ ϟ x   optional stamina     │
│ ♢ x   optional MP          │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Status effect / duration   │
│ Playability / removal rule │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1e.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1e.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1e.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1e.header.title | remaining header width | 100% header height | WC1e.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1e.header.state | content-fit | 100% header height | WC1e.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1e.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1e.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1e.art.tags | available art width minus shared inset | content-fit within art band | WC1e.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1e.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1e.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1e.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1e.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1e.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1e.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1e.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1e.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1e.body.detail1 | 100% usable body width | content-fit within body band | WC1e.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Status effect / duration |
| WC1e.body.detail2 | 100% usable body width | content-fit within body band | WC1e.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Playability / removal rule |
| WC1e.body.blocker | 100% usable body width | content-fit within body band | WC1e.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1e.selection.outline | 100% card width | 100% card height | WC1e.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1e.selection.info | config.components.target.minRem | config.components.target.minRem | WC1e.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ ◆ x   [Art / portrait]   │
│ ϟ x   optional stamina   │
│ ♢ x   optional MP        │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Status effect / duration │
│ Playability / removal    │
│ rule                     │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1e.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1e.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1e.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1e.header.title | remaining header width | 100% header height | WC1e.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1e.header.state | content-fit | 100% header height | WC1e.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1e.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1e.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1e.art.tags | available art width minus shared inset | content-fit within art band | WC1e.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1e.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1e.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1e.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1e.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1e.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1e.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1e.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1e.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1e.body.detail1 | 100% usable body width | content-fit within body band | WC1e.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Status effect / duration |
| WC1e.body.detail2 | 100% usable body width | content-fit within body band | WC1e.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Playability / removal rule |
| WC1e.body.blocker | 100% usable body width | content-fit within body band | WC1e.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1e.selection.outline | 100% card width | 100% card height | WC1e.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1e.selection.info | config.components.target.minRem | config.components.target.minRem | WC1e.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ ◆ x   [Art / portrait]   │
│ ϟ x   optional stamina   │
│ ♢ x   optional MP        │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Status effect / duration │
│ Playability / removal    │
│ rule                     │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC1e.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC1e.header | 100% card width | config.cards.geometry.bands.header% of card height | WC1e.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC1e.header.title | remaining header width | 100% header height | WC1e.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC1e.header.state | content-fit | 100% header height | WC1e.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC1e.art | 100% card width | config.cards.geometry.bands.art% of card height | WC1e.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC1e.art.tags | available art width minus shared inset | content-fit within art band | WC1e.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC1e.body | 100% card width | config.cards.geometry.bands.body% of card height | WC1e.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC1e.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC1e.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC1e.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC1e.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC1e.costs | config.cards.costs.railWidthRem | content-fit from active projected costs | WC1e.frame | top-left | start / start | normal grid flow | 0; shared gap between siblings | Art top-left at configured header band plus inset; outlined icon and number only, no boxes; action/stamina/MP stack remains exposed in a fanned hand |
| WC1e.body.detail1 | 100% usable body width | content-fit within body band | WC1e.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Status effect / duration |
| WC1e.body.detail2 | 100% usable body width | content-fit within body band | WC1e.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Playability / removal rule |
| WC1e.body.blocker | 100% usable body width | content-fit within body band | WC1e.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC1e.selection.outline | 100% card width | 100% card height | WC1e.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC1e.selection.info | config.components.target.minRem | config.components.target.minRem | WC1e.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC1
REQUIRED PROPOSED TAGS: ability:status
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC1e
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC1e.body.detail1 ← registered provider for Status effect / duration
    WC1e.body.detail2 ← registered provider for Playability / removal rule
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Inspect") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
This is a playing-card subtype, distinct from a combat status indicator.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC1e",
  "tags": [
    "ability:status"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC1",
    "name": "Dazed",
    "glyph": "⌁",
    "headerState": "Unplayable",
    "rules": "A temporary condition occupying a draw.",
    "facts": [
      [
        "Lifetime",
        "Projected by the domain"
      ]
    ],
    "tags": [
      "status"
    ],
    "availability": "No play action."
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC2: Possession card

**Parent: WC0.** Common item identity, ownership and instance state; tags add supported operations.

**Construction tags (proposed):** `card-kind:possession`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Ownership / quantity           │
│ Capabilities / requirements    │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2.header.title | remaining header width | 100% header height | WC2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2.header.state | content-fit | 100% header height | WC2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2.art.tags | available art width minus shared inset | content-fit within art band | WC2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2.body.detail1 | 100% usable body width | content-fit within body band | WC2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Ownership / quantity |
| WC2.body.detail2 | 100% usable body width | content-fit within body band | WC2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Capabilities / requirements |
| WC2.body.blocker | 100% usable body width | content-fit within body band | WC2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2.selection.outline | 100% card width | 100% card height | WC2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Ownership / quantity       │
│ Capabilities /             │
│ requirements               │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2.header.title | remaining header width | 100% header height | WC2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2.header.state | content-fit | 100% header height | WC2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2.art.tags | available art width minus shared inset | content-fit within art band | WC2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2.body.detail1 | 100% usable body width | content-fit within body band | WC2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Ownership / quantity |
| WC2.body.detail2 | 100% usable body width | content-fit within body band | WC2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Capabilities / requirements |
| WC2.body.blocker | 100% usable body width | content-fit within body band | WC2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2.selection.outline | 100% card width | 100% card height | WC2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Ownership / quantity     │
│ Capabilities /           │
│ requirements             │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2.header.title | remaining header width | 100% header height | WC2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2.header.state | content-fit | 100% header height | WC2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2.art.tags | available art width minus shared inset | content-fit within art band | WC2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2.body.detail1 | 100% usable body width | content-fit within body band | WC2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Ownership / quantity |
| WC2.body.detail2 | 100% usable body width | content-fit within body band | WC2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Capabilities / requirements |
| WC2.body.blocker | 100% usable body width | content-fit within body band | WC2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2.selection.outline | 100% card width | 100% card height | WC2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Ownership / quantity     │
│ Capabilities /           │
│ requirements             │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2.header.title | remaining header width | 100% header height | WC2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2.header.state | content-fit | 100% header height | WC2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2.art.tags | available art width minus shared inset | content-fit within art band | WC2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2.body.detail1 | 100% usable body width | content-fit within body band | WC2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Ownership / quantity |
| WC2.body.detail2 | 100% usable body width | content-fit within body band | WC2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Capabilities / requirements |
| WC2.body.blocker | 100% usable body width | content-fit within body band | WC2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2.selection.outline | 100% card width | 100% card height | WC2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC0
REQUIRED PROPOSED TAGS: card-kind:possession
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC2
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC2.body.detail1 ← registered provider for Ownership / quantity
    WC2.body.detail2 ← registered provider for Capabilities / requirements
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Inspect") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Common item identity, ownership and instance state; tags add supported operations.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC2",
  "tags": [
    "card-kind:possession"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC0",
    "name": "Traveler’s Keepsake",
    "kind": "item",
    "glyph": "◆",
    "rules": "An owned item with authored effects.",
    "facts": [
      [
        "Ownership",
        "Carried"
      ]
    ],
    "tags": [],
    "references": [
      "src/model/equipmentCard.js",
      "src/ui/components/equipmentCard.js"
    ]
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC2a: Equipment card

**Parent: WC2.** The equipment base does not decide whether the item is weapon or armor by hard-coded ID.

**Construction tags (proposed):** `item-kind:equipment`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Slot / requirements            │
│ Equipped comparison            │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2a.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2a.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2a.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2a.header.title | remaining header width | 100% header height | WC2a.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2a.header.state | content-fit | 100% header height | WC2a.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2a.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2a.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2a.art.tags | available art width minus shared inset | content-fit within art band | WC2a.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2a.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2a.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2a.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2a.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2a.body.detail1 | 100% usable body width | content-fit within body band | WC2a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Slot / requirements |
| WC2a.body.detail2 | 100% usable body width | content-fit within body band | WC2a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Equipped comparison |
| WC2a.body.blocker | 100% usable body width | content-fit within body band | WC2a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2a.selection.outline | 100% card width | 100% card height | WC2a.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2a.selection.info | config.components.target.minRem | config.components.target.minRem | WC2a.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Slot / requirements        │
│ Equipped comparison        │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2a.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2a.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2a.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2a.header.title | remaining header width | 100% header height | WC2a.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2a.header.state | content-fit | 100% header height | WC2a.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2a.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2a.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2a.art.tags | available art width minus shared inset | content-fit within art band | WC2a.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2a.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2a.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2a.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2a.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2a.body.detail1 | 100% usable body width | content-fit within body band | WC2a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Slot / requirements |
| WC2a.body.detail2 | 100% usable body width | content-fit within body band | WC2a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Equipped comparison |
| WC2a.body.blocker | 100% usable body width | content-fit within body band | WC2a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2a.selection.outline | 100% card width | 100% card height | WC2a.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2a.selection.info | config.components.target.minRem | config.components.target.minRem | WC2a.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Slot / requirements      │
│ Equipped comparison      │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2a.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2a.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2a.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2a.header.title | remaining header width | 100% header height | WC2a.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2a.header.state | content-fit | 100% header height | WC2a.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2a.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2a.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2a.art.tags | available art width minus shared inset | content-fit within art band | WC2a.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2a.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2a.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2a.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2a.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2a.body.detail1 | 100% usable body width | content-fit within body band | WC2a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Slot / requirements |
| WC2a.body.detail2 | 100% usable body width | content-fit within body band | WC2a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Equipped comparison |
| WC2a.body.blocker | 100% usable body width | content-fit within body band | WC2a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2a.selection.outline | 100% card width | 100% card height | WC2a.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2a.selection.info | config.components.target.minRem | config.components.target.minRem | WC2a.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Slot / requirements      │
│ Equipped comparison      │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2a.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2a.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2a.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2a.header.title | remaining header width | 100% header height | WC2a.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2a.header.state | content-fit | 100% header height | WC2a.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2a.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2a.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2a.art.tags | available art width minus shared inset | content-fit within art band | WC2a.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2a.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2a.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2a.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2a.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2a.body.detail1 | 100% usable body width | content-fit within body band | WC2a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Slot / requirements |
| WC2a.body.detail2 | 100% usable body width | content-fit within body band | WC2a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Equipped comparison |
| WC2a.body.blocker | 100% usable body width | content-fit within body band | WC2a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2a.selection.outline | 100% card width | 100% card height | WC2a.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2a.selection.info | config.components.target.minRem | config.components.target.minRem | WC2a.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC2
REQUIRED PROPOSED TAGS: item-kind:equipment
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC2a
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC2a.body.detail1 ← registered provider for Slot / requirements
    WC2a.body.detail2 ← registered provider for Equipped comparison
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Equip") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
The equipment base does not decide whether the item is weapon or armor by hard-coded ID.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC2a",
  "tags": [
    "item-kind:equipment"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC2",
    "name": "Iron Blade",
    "kind": "equipment",
    "glyph": "⚔",
    "headerState": "Equipped",
    "rules": "A balanced blade with a projected attack bonus.",
    "facts": [
      [
        "Attack",
        "+2"
      ],
      [
        "Requirement",
        "Strength 8"
      ]
    ],
    "tags": [
      "weapon"
    ],
    "construction": [
      "feature:rules",
      "feature:facts",
      "feature:availability"
    ],
    "availability": "Compatible with the selected weapon slot."
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC2a1: Weapon card

**Parent: WC2a.** Weapon-specific rows attach by tags; values come from loadout/equipment projections.

**Construction tags (proposed):** `equipment-kind:weapon`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Damage / scaling               │
│ Hand / requirements            │
│ Granted card package           │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2a1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2a1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2a1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2a1.header.title | remaining header width | 100% header height | WC2a1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2a1.header.state | content-fit | 100% header height | WC2a1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2a1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2a1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2a1.art.tags | available art width minus shared inset | content-fit within art band | WC2a1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2a1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2a1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2a1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2a1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2a1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2a1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2a1.body.detail1 | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Damage / scaling |
| WC2a1.body.detail2 | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Hand / requirements |
| WC2a1.body.detail3 | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Granted card package |
| WC2a1.body.blocker | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2a1.selection.outline | 100% card width | 100% card height | WC2a1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2a1.selection.info | config.components.target.minRem | config.components.target.minRem | WC2a1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Damage / scaling           │
│ Hand / requirements        │
│ Granted card package       │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2a1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2a1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2a1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2a1.header.title | remaining header width | 100% header height | WC2a1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2a1.header.state | content-fit | 100% header height | WC2a1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2a1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2a1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2a1.art.tags | available art width minus shared inset | content-fit within art band | WC2a1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2a1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2a1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2a1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2a1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2a1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2a1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2a1.body.detail1 | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Damage / scaling |
| WC2a1.body.detail2 | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Hand / requirements |
| WC2a1.body.detail3 | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Granted card package |
| WC2a1.body.blocker | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2a1.selection.outline | 100% card width | 100% card height | WC2a1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2a1.selection.info | config.components.target.minRem | config.components.target.minRem | WC2a1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Damage / scaling         │
│ Hand / requirements      │
│ Granted card package     │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2a1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2a1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2a1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2a1.header.title | remaining header width | 100% header height | WC2a1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2a1.header.state | content-fit | 100% header height | WC2a1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2a1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2a1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2a1.art.tags | available art width minus shared inset | content-fit within art band | WC2a1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2a1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2a1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2a1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2a1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2a1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2a1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2a1.body.detail1 | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Damage / scaling |
| WC2a1.body.detail2 | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Hand / requirements |
| WC2a1.body.detail3 | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Granted card package |
| WC2a1.body.blocker | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2a1.selection.outline | 100% card width | 100% card height | WC2a1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2a1.selection.info | config.components.target.minRem | config.components.target.minRem | WC2a1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Damage / scaling         │
│ Hand / requirements      │
│ Granted card package     │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2a1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2a1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2a1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2a1.header.title | remaining header width | 100% header height | WC2a1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2a1.header.state | content-fit | 100% header height | WC2a1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2a1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2a1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2a1.art.tags | available art width minus shared inset | content-fit within art band | WC2a1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2a1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2a1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2a1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2a1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2a1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2a1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2a1.body.detail1 | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Damage / scaling |
| WC2a1.body.detail2 | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Hand / requirements |
| WC2a1.body.detail3 | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Granted card package |
| WC2a1.body.blocker | 100% usable body width | content-fit within body band | WC2a1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2a1.selection.outline | 100% card width | 100% card height | WC2a1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2a1.selection.info | config.components.target.minRem | config.components.target.minRem | WC2a1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC2a
REQUIRED PROPOSED TAGS: equipment-kind:weapon
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC2a1
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC2a1.body.detail1 ← registered provider for Damage / scaling
    WC2a1.body.detail2 ← registered provider for Hand / requirements
    WC2a1.body.detail3 ← registered provider for Granted card package
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Equip") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Weapon-specific rows attach by tags; values come from loadout/equipment projections.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC2a1",
  "tags": [
    "equipment-kind:weapon"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC2a",
    "name": "Iron Blade"
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC2a2: Armor card

**Parent: WC2a.** Armor shares equipment slots and actions; only supported defense/modifier components differ.

**Construction tags (proposed):** `equipment-kind:armor`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Defense / resistance           │
│ Weight / requirements          │
│ Granted modifiers              │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2a2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2a2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2a2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2a2.header.title | remaining header width | 100% header height | WC2a2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2a2.header.state | content-fit | 100% header height | WC2a2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2a2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2a2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2a2.art.tags | available art width minus shared inset | content-fit within art band | WC2a2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2a2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2a2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2a2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2a2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2a2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2a2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2a2.body.detail1 | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Defense / resistance |
| WC2a2.body.detail2 | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Weight / requirements |
| WC2a2.body.detail3 | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Granted modifiers |
| WC2a2.body.blocker | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2a2.selection.outline | 100% card width | 100% card height | WC2a2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2a2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2a2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Defense / resistance       │
│ Weight / requirements      │
│ Granted modifiers          │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2a2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2a2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2a2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2a2.header.title | remaining header width | 100% header height | WC2a2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2a2.header.state | content-fit | 100% header height | WC2a2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2a2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2a2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2a2.art.tags | available art width minus shared inset | content-fit within art band | WC2a2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2a2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2a2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2a2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2a2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2a2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2a2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2a2.body.detail1 | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Defense / resistance |
| WC2a2.body.detail2 | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Weight / requirements |
| WC2a2.body.detail3 | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Granted modifiers |
| WC2a2.body.blocker | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2a2.selection.outline | 100% card width | 100% card height | WC2a2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2a2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2a2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Defense / resistance     │
│ Weight / requirements    │
│ Granted modifiers        │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2a2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2a2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2a2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2a2.header.title | remaining header width | 100% header height | WC2a2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2a2.header.state | content-fit | 100% header height | WC2a2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2a2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2a2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2a2.art.tags | available art width minus shared inset | content-fit within art band | WC2a2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2a2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2a2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2a2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2a2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2a2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2a2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2a2.body.detail1 | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Defense / resistance |
| WC2a2.body.detail2 | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Weight / requirements |
| WC2a2.body.detail3 | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Granted modifiers |
| WC2a2.body.blocker | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2a2.selection.outline | 100% card width | 100% card height | WC2a2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2a2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2a2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Defense / resistance     │
│ Weight / requirements    │
│ Granted modifiers        │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2a2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2a2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2a2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2a2.header.title | remaining header width | 100% header height | WC2a2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2a2.header.state | content-fit | 100% header height | WC2a2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2a2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2a2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2a2.art.tags | available art width minus shared inset | content-fit within art band | WC2a2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2a2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2a2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2a2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2a2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2a2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2a2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2a2.body.detail1 | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Defense / resistance |
| WC2a2.body.detail2 | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Weight / requirements |
| WC2a2.body.detail3 | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Granted modifiers |
| WC2a2.body.blocker | 100% usable body width | content-fit within body band | WC2a2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2a2.selection.outline | 100% card width | 100% card height | WC2a2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2a2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2a2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC2a
REQUIRED PROPOSED TAGS: equipment-kind:armor
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC2a2
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC2a2.body.detail1 ← registered provider for Defense / resistance
    WC2a2.body.detail2 ← registered provider for Weight / requirements
    WC2a2.body.detail3 ← registered provider for Granted modifiers
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Equip") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Armor shares equipment slots and actions; only supported defense/modifier components differ.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC2a2",
  "tags": [
    "equipment-kind:armor"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC2a",
    "name": "Travel Armor",
    "glyph": "⛨",
    "rules": "Light protection for the road.",
    "facts": [
      [
        "Protection",
        "+3"
      ],
      [
        "Requirement",
        "Constitution 8"
      ]
    ],
    "tags": [
      "armor"
    ],
    "availability": "Compatible with the selected armor slot."
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC2b: Relic card

**Parent: WC2.** Relic actions vary by context; do not add Equip where the domain treats possession as activation.

**Construction tags (proposed):** `item-kind:relic`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Relic effect                   │
│ Acquisition / equip state      │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2b.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2b.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2b.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2b.header.title | remaining header width | 100% header height | WC2b.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2b.header.state | content-fit | 100% header height | WC2b.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2b.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2b.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2b.art.tags | available art width minus shared inset | content-fit within art band | WC2b.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2b.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2b.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2b.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2b.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2b.body.detail1 | 100% usable body width | content-fit within body band | WC2b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Relic effect |
| WC2b.body.detail2 | 100% usable body width | content-fit within body band | WC2b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Acquisition / equip state |
| WC2b.body.blocker | 100% usable body width | content-fit within body band | WC2b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2b.selection.outline | 100% card width | 100% card height | WC2b.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2b.selection.info | config.components.target.minRem | config.components.target.minRem | WC2b.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Relic effect               │
│ Acquisition / equip state  │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2b.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2b.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2b.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2b.header.title | remaining header width | 100% header height | WC2b.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2b.header.state | content-fit | 100% header height | WC2b.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2b.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2b.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2b.art.tags | available art width minus shared inset | content-fit within art band | WC2b.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2b.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2b.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2b.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2b.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2b.body.detail1 | 100% usable body width | content-fit within body band | WC2b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Relic effect |
| WC2b.body.detail2 | 100% usable body width | content-fit within body band | WC2b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Acquisition / equip state |
| WC2b.body.blocker | 100% usable body width | content-fit within body band | WC2b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2b.selection.outline | 100% card width | 100% card height | WC2b.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2b.selection.info | config.components.target.minRem | config.components.target.minRem | WC2b.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Relic effect             │
│ Acquisition / equip      │
│ state                    │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2b.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2b.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2b.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2b.header.title | remaining header width | 100% header height | WC2b.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2b.header.state | content-fit | 100% header height | WC2b.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2b.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2b.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2b.art.tags | available art width minus shared inset | content-fit within art band | WC2b.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2b.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2b.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2b.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2b.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2b.body.detail1 | 100% usable body width | content-fit within body band | WC2b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Relic effect |
| WC2b.body.detail2 | 100% usable body width | content-fit within body band | WC2b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Acquisition / equip state |
| WC2b.body.blocker | 100% usable body width | content-fit within body band | WC2b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2b.selection.outline | 100% card width | 100% card height | WC2b.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2b.selection.info | config.components.target.minRem | config.components.target.minRem | WC2b.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Relic effect             │
│ Acquisition / equip      │
│ state                    │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2b.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2b.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2b.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2b.header.title | remaining header width | 100% header height | WC2b.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2b.header.state | content-fit | 100% header height | WC2b.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2b.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2b.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2b.art.tags | available art width minus shared inset | content-fit within art band | WC2b.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2b.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2b.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2b.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2b.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2b.body.detail1 | 100% usable body width | content-fit within body band | WC2b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Relic effect |
| WC2b.body.detail2 | 100% usable body width | content-fit within body band | WC2b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Acquisition / equip state |
| WC2b.body.blocker | 100% usable body width | content-fit within body band | WC2b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2b.selection.outline | 100% card width | 100% card height | WC2b.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2b.selection.info | config.components.target.minRem | config.components.target.minRem | WC2b.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC2
REQUIRED PROPOSED TAGS: item-kind:relic
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC2b
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC2b.body.detail1 ← registered provider for Relic effect
    WC2b.body.detail2 ← registered provider for Acquisition / equip state
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Inspect") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Relic actions vary by context; do not add Equip where the domain treats possession as activation.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC2b",
  "tags": [
    "item-kind:relic"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC2",
    "name": "Ashen Seal",
    "kind": "relic",
    "glyph": "✥",
    "headerState": "Passive",
    "rules": "Grants its authored benefit while active.",
    "facts": [
      [
        "Activation",
        "While owned"
      ]
    ],
    "tags": [
      "relic",
      "passive"
    ],
    "references": [
      "src/ui/components/collectibleCard.js",
      "src/ui/components/equipmentCard.js"
    ]
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC2b1: Passive relic

**Parent: WC2b.** Show existing passive definitions and current applied state.

**Construction tags (proposed):** `effect-mode:passive`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Passive modifiers              │
│ Affected resources / stats     │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2b1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2b1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2b1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2b1.header.title | remaining header width | 100% header height | WC2b1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2b1.header.state | content-fit | 100% header height | WC2b1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2b1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2b1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2b1.art.tags | available art width minus shared inset | content-fit within art band | WC2b1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2b1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2b1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2b1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2b1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2b1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2b1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2b1.body.detail1 | 100% usable body width | content-fit within body band | WC2b1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Passive modifiers |
| WC2b1.body.detail2 | 100% usable body width | content-fit within body band | WC2b1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Affected resources / stats |
| WC2b1.body.blocker | 100% usable body width | content-fit within body band | WC2b1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2b1.selection.outline | 100% card width | 100% card height | WC2b1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2b1.selection.info | config.components.target.minRem | config.components.target.minRem | WC2b1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Passive modifiers          │
│ Affected resources / stats │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2b1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2b1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2b1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2b1.header.title | remaining header width | 100% header height | WC2b1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2b1.header.state | content-fit | 100% header height | WC2b1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2b1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2b1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2b1.art.tags | available art width minus shared inset | content-fit within art band | WC2b1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2b1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2b1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2b1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2b1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2b1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2b1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2b1.body.detail1 | 100% usable body width | content-fit within body band | WC2b1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Passive modifiers |
| WC2b1.body.detail2 | 100% usable body width | content-fit within body band | WC2b1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Affected resources / stats |
| WC2b1.body.blocker | 100% usable body width | content-fit within body band | WC2b1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2b1.selection.outline | 100% card width | 100% card height | WC2b1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2b1.selection.info | config.components.target.minRem | config.components.target.minRem | WC2b1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Passive modifiers        │
│ Affected resources /     │
│ stats                    │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2b1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2b1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2b1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2b1.header.title | remaining header width | 100% header height | WC2b1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2b1.header.state | content-fit | 100% header height | WC2b1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2b1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2b1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2b1.art.tags | available art width minus shared inset | content-fit within art band | WC2b1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2b1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2b1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2b1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2b1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2b1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2b1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2b1.body.detail1 | 100% usable body width | content-fit within body band | WC2b1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Passive modifiers |
| WC2b1.body.detail2 | 100% usable body width | content-fit within body band | WC2b1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Affected resources / stats |
| WC2b1.body.blocker | 100% usable body width | content-fit within body band | WC2b1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2b1.selection.outline | 100% card width | 100% card height | WC2b1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2b1.selection.info | config.components.target.minRem | config.components.target.minRem | WC2b1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Passive modifiers        │
│ Affected resources /     │
│ stats                    │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2b1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2b1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2b1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2b1.header.title | remaining header width | 100% header height | WC2b1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2b1.header.state | content-fit | 100% header height | WC2b1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2b1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2b1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2b1.art.tags | available art width minus shared inset | content-fit within art band | WC2b1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2b1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2b1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2b1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2b1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2b1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2b1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2b1.body.detail1 | 100% usable body width | content-fit within body band | WC2b1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Passive modifiers |
| WC2b1.body.detail2 | 100% usable body width | content-fit within body band | WC2b1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Affected resources / stats |
| WC2b1.body.blocker | 100% usable body width | content-fit within body band | WC2b1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2b1.selection.outline | 100% card width | 100% card height | WC2b1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2b1.selection.info | config.components.target.minRem | config.components.target.minRem | WC2b1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC2b
REQUIRED PROPOSED TAGS: effect-mode:passive
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC2b1
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC2b1.body.detail1 ← registered provider for Passive modifiers
    WC2b1.body.detail2 ← registered provider for Affected resources / stats
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Inspect") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Show existing passive definitions and current applied state.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC2b1",
  "tags": [
    "effect-mode:passive"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC2b",
    "name": "Ashen Seal"
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC2b2: Triggered relic

**Parent: WC2b.** Multiple effect modes may coexist through components; do not force a false exclusive category.

**Construction tags (proposed):** `effect-mode:triggered`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Trigger condition              │
│ Effect / limit / cooldown      │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2b2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2b2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2b2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2b2.header.title | remaining header width | 100% header height | WC2b2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2b2.header.state | content-fit | 100% header height | WC2b2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2b2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2b2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2b2.art.tags | available art width minus shared inset | content-fit within art band | WC2b2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2b2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2b2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2b2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2b2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2b2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2b2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2b2.body.detail1 | 100% usable body width | content-fit within body band | WC2b2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Trigger condition |
| WC2b2.body.detail2 | 100% usable body width | content-fit within body band | WC2b2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Effect / limit / cooldown |
| WC2b2.body.blocker | 100% usable body width | content-fit within body band | WC2b2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2b2.selection.outline | 100% card width | 100% card height | WC2b2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2b2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2b2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Trigger condition          │
│ Effect / limit / cooldown  │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2b2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2b2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2b2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2b2.header.title | remaining header width | 100% header height | WC2b2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2b2.header.state | content-fit | 100% header height | WC2b2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2b2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2b2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2b2.art.tags | available art width minus shared inset | content-fit within art band | WC2b2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2b2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2b2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2b2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2b2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2b2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2b2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2b2.body.detail1 | 100% usable body width | content-fit within body band | WC2b2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Trigger condition |
| WC2b2.body.detail2 | 100% usable body width | content-fit within body band | WC2b2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Effect / limit / cooldown |
| WC2b2.body.blocker | 100% usable body width | content-fit within body band | WC2b2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2b2.selection.outline | 100% card width | 100% card height | WC2b2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2b2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2b2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Trigger condition        │
│ Effect / limit /         │
│ cooldown                 │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2b2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2b2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2b2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2b2.header.title | remaining header width | 100% header height | WC2b2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2b2.header.state | content-fit | 100% header height | WC2b2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2b2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2b2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2b2.art.tags | available art width minus shared inset | content-fit within art band | WC2b2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2b2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2b2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2b2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2b2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2b2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2b2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2b2.body.detail1 | 100% usable body width | content-fit within body band | WC2b2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Trigger condition |
| WC2b2.body.detail2 | 100% usable body width | content-fit within body band | WC2b2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Effect / limit / cooldown |
| WC2b2.body.blocker | 100% usable body width | content-fit within body band | WC2b2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2b2.selection.outline | 100% card width | 100% card height | WC2b2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2b2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2b2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Trigger condition        │
│ Effect / limit /         │
│ cooldown                 │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2b2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2b2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2b2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2b2.header.title | remaining header width | 100% header height | WC2b2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2b2.header.state | content-fit | 100% header height | WC2b2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2b2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2b2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2b2.art.tags | available art width minus shared inset | content-fit within art band | WC2b2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2b2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2b2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2b2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2b2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2b2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2b2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2b2.body.detail1 | 100% usable body width | content-fit within body band | WC2b2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Trigger condition |
| WC2b2.body.detail2 | 100% usable body width | content-fit within body band | WC2b2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Effect / limit / cooldown |
| WC2b2.body.blocker | 100% usable body width | content-fit within body band | WC2b2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2b2.selection.outline | 100% card width | 100% card height | WC2b2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2b2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2b2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC2b
REQUIRED PROPOSED TAGS: effect-mode:triggered
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC2b2
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC2b2.body.detail1 ← registered provider for Trigger condition
    WC2b2.body.detail2 ← registered provider for Effect / limit / cooldown
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Inspect") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Multiple effect modes may coexist through components; do not force a false exclusive category.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC2b2",
  "tags": [
    "effect-mode:triggered"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC2b",
    "name": "Watchkeeper Bell",
    "glyph": "♧",
    "headerState": "Triggered",
    "rules": "Grants its authored benefit when guard breaks.",
    "facts": [
      [
        "Trigger",
        "Guard break"
      ],
      [
        "Cooldown",
        "Once per encounter"
      ]
    ],
    "tags": [
      "relic",
      "triggered"
    ]
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC2c: Consumable card

**Parent: WC2.** Model depleted vs unavailable separately; inventory state remains authoritative.

**Construction tags (proposed):** `item-kind:consumable`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Charges / quantity             │
│ Use effect / eligibility       │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c.header.title | remaining header width | 100% header height | WC2c.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c.header.state | content-fit | 100% header height | WC2c.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c.art.tags | available art width minus shared inset | content-fit within art band | WC2c.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c.body.detail1 | 100% usable body width | content-fit within body band | WC2c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Charges / quantity |
| WC2c.body.detail2 | 100% usable body width | content-fit within body band | WC2c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Use effect / eligibility |
| WC2c.body.blocker | 100% usable body width | content-fit within body band | WC2c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c.selection.outline | 100% card width | 100% card height | WC2c.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Charges / quantity         │
│ Use effect / eligibility   │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c.header.title | remaining header width | 100% header height | WC2c.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c.header.state | content-fit | 100% header height | WC2c.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c.art.tags | available art width minus shared inset | content-fit within art band | WC2c.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c.body.detail1 | 100% usable body width | content-fit within body band | WC2c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Charges / quantity |
| WC2c.body.detail2 | 100% usable body width | content-fit within body band | WC2c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Use effect / eligibility |
| WC2c.body.blocker | 100% usable body width | content-fit within body band | WC2c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c.selection.outline | 100% card width | 100% card height | WC2c.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Charges / quantity       │
│ Use effect / eligibility │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c.header.title | remaining header width | 100% header height | WC2c.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c.header.state | content-fit | 100% header height | WC2c.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c.art.tags | available art width minus shared inset | content-fit within art band | WC2c.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c.body.detail1 | 100% usable body width | content-fit within body band | WC2c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Charges / quantity |
| WC2c.body.detail2 | 100% usable body width | content-fit within body band | WC2c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Use effect / eligibility |
| WC2c.body.blocker | 100% usable body width | content-fit within body band | WC2c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c.selection.outline | 100% card width | 100% card height | WC2c.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Charges / quantity       │
│ Use effect / eligibility │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c.header.title | remaining header width | 100% header height | WC2c.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c.header.state | content-fit | 100% header height | WC2c.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c.art.tags | available art width minus shared inset | content-fit within art band | WC2c.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c.body.detail1 | 100% usable body width | content-fit within body band | WC2c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Charges / quantity |
| WC2c.body.detail2 | 100% usable body width | content-fit within body band | WC2c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Use effect / eligibility |
| WC2c.body.blocker | 100% usable body width | content-fit within body band | WC2c.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c.selection.outline | 100% card width | 100% card height | WC2c.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC2
REQUIRED PROPOSED TAGS: item-kind:consumable
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC2c
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC2c.body.detail1 ← registered provider for Charges / quantity
    WC2c.body.detail2 ← registered provider for Use effect / eligibility
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Use") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Model depleted vs unavailable separately; inventory state remains authoritative.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC2c",
  "tags": [
    "item-kind:consumable"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC2",
    "name": "Healing Draught",
    "kind": "consumable",
    "glyph": "⚗",
    "headerState": "1 charge",
    "rules": "Restore 20 health, limited by maximum health.",
    "facts": [
      [
        "Charges",
        "1 / 1"
      ]
    ],
    "tags": [
      "consumable",
      "healing"
    ],
    "construction": [
      "feature:rules",
      "feature:facts",
      "feature:availability"
    ],
    "availability": "Available when health is missing.",
    "references": [
      "src/ui/components/collectibleCard.js",
      "src/ui/components/flask.js"
    ],
    "flavor": "A little warmth against the ash."
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC2c1: Healing consumable

**Parent: WC2c.** Bind existing heal preview/cap rules.

**Construction tags (proposed):** `effect-purpose:healing`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Healing preview                │
│ Charges / availability         │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c1.header.title | remaining header width | 100% header height | WC2c1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c1.header.state | content-fit | 100% header height | WC2c1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c1.art.tags | available art width minus shared inset | content-fit within art band | WC2c1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c1.body.detail1 | 100% usable body width | content-fit within body band | WC2c1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Healing preview |
| WC2c1.body.detail2 | 100% usable body width | content-fit within body band | WC2c1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Charges / availability |
| WC2c1.body.blocker | 100% usable body width | content-fit within body band | WC2c1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c1.selection.outline | 100% card width | 100% card height | WC2c1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c1.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Healing preview            │
│ Charges / availability     │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c1.header.title | remaining header width | 100% header height | WC2c1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c1.header.state | content-fit | 100% header height | WC2c1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c1.art.tags | available art width minus shared inset | content-fit within art band | WC2c1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c1.body.detail1 | 100% usable body width | content-fit within body band | WC2c1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Healing preview |
| WC2c1.body.detail2 | 100% usable body width | content-fit within body band | WC2c1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Charges / availability |
| WC2c1.body.blocker | 100% usable body width | content-fit within body band | WC2c1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c1.selection.outline | 100% card width | 100% card height | WC2c1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c1.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Healing preview          │
│ Charges / availability   │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c1.header.title | remaining header width | 100% header height | WC2c1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c1.header.state | content-fit | 100% header height | WC2c1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c1.art.tags | available art width minus shared inset | content-fit within art band | WC2c1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c1.body.detail1 | 100% usable body width | content-fit within body band | WC2c1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Healing preview |
| WC2c1.body.detail2 | 100% usable body width | content-fit within body band | WC2c1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Charges / availability |
| WC2c1.body.blocker | 100% usable body width | content-fit within body band | WC2c1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c1.selection.outline | 100% card width | 100% card height | WC2c1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c1.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Healing preview          │
│ Charges / availability   │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c1.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c1.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c1.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c1.header.title | remaining header width | 100% header height | WC2c1.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c1.header.state | content-fit | 100% header height | WC2c1.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c1.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c1.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c1.art.tags | available art width minus shared inset | content-fit within art band | WC2c1.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c1.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c1.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c1.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c1.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c1.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c1.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c1.body.detail1 | 100% usable body width | content-fit within body band | WC2c1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Healing preview |
| WC2c1.body.detail2 | 100% usable body width | content-fit within body band | WC2c1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Charges / availability |
| WC2c1.body.blocker | 100% usable body width | content-fit within body band | WC2c1.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c1.selection.outline | 100% card width | 100% card height | WC2c1.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c1.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c1.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC2c
REQUIRED PROPOSED TAGS: effect-purpose:healing
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC2c1
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC2c1.body.detail1 ← registered provider for Healing preview
    WC2c1.body.detail2 ← registered provider for Charges / availability
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Use") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Bind existing heal preview/cap rules.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC2c1",
  "tags": [
    "effect-purpose:healing"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC2c",
    "name": "Healing Draught"
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC2c2: Resource consumable

**Parent: WC2c.** Resource identity selects its registered semantic color, not primary green.

**Construction tags (proposed):** `effect-purpose:resource`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Resource restoration           │
│ Charges / availability         │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c2.header.title | remaining header width | 100% header height | WC2c2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c2.header.state | content-fit | 100% header height | WC2c2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c2.art.tags | available art width minus shared inset | content-fit within art band | WC2c2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c2.body.detail1 | 100% usable body width | content-fit within body band | WC2c2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Resource restoration |
| WC2c2.body.detail2 | 100% usable body width | content-fit within body band | WC2c2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Charges / availability |
| WC2c2.body.blocker | 100% usable body width | content-fit within body band | WC2c2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c2.selection.outline | 100% card width | 100% card height | WC2c2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Resource restoration       │
│ Charges / availability     │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c2.header.title | remaining header width | 100% header height | WC2c2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c2.header.state | content-fit | 100% header height | WC2c2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c2.art.tags | available art width minus shared inset | content-fit within art band | WC2c2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c2.body.detail1 | 100% usable body width | content-fit within body band | WC2c2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Resource restoration |
| WC2c2.body.detail2 | 100% usable body width | content-fit within body band | WC2c2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Charges / availability |
| WC2c2.body.blocker | 100% usable body width | content-fit within body band | WC2c2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c2.selection.outline | 100% card width | 100% card height | WC2c2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Resource restoration     │
│ Charges / availability   │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c2.header.title | remaining header width | 100% header height | WC2c2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c2.header.state | content-fit | 100% header height | WC2c2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c2.art.tags | available art width minus shared inset | content-fit within art band | WC2c2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c2.body.detail1 | 100% usable body width | content-fit within body band | WC2c2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Resource restoration |
| WC2c2.body.detail2 | 100% usable body width | content-fit within body band | WC2c2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Charges / availability |
| WC2c2.body.blocker | 100% usable body width | content-fit within body band | WC2c2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c2.selection.outline | 100% card width | 100% card height | WC2c2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Resource restoration     │
│ Charges / availability   │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c2.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c2.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c2.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c2.header.title | remaining header width | 100% header height | WC2c2.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c2.header.state | content-fit | 100% header height | WC2c2.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c2.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c2.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c2.art.tags | available art width minus shared inset | content-fit within art band | WC2c2.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c2.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c2.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c2.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c2.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c2.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c2.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c2.body.detail1 | 100% usable body width | content-fit within body band | WC2c2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Resource restoration |
| WC2c2.body.detail2 | 100% usable body width | content-fit within body band | WC2c2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Charges / availability |
| WC2c2.body.blocker | 100% usable body width | content-fit within body band | WC2c2.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c2.selection.outline | 100% card width | 100% card height | WC2c2.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c2.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c2.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC2c
REQUIRED PROPOSED TAGS: effect-purpose:resource
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC2c2
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC2c2.body.detail1 ← registered provider for Resource restoration
    WC2c2.body.detail2 ← registered provider for Charges / availability
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Use") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Resource identity selects its registered semantic color, not primary green.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC2c2",
  "tags": [
    "effect-purpose:resource"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC2c",
    "name": "Azure Draught",
    "rules": "Restore 2 resource, limited by its maximum.",
    "tags": [
      "consumable",
      "resource"
    ],
    "availability": "Available when resource is missing."
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC2c3: Utility consumable

**Parent: WC2c.** Targeted use follows existing targeting/confirmation command flow.

**Construction tags (proposed):** `effect-purpose:utility`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Utility effect / target        │
│ Quantity / availability        │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c3.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c3.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c3.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c3.header.title | remaining header width | 100% header height | WC2c3.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c3.header.state | content-fit | 100% header height | WC2c3.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c3.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c3.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c3.art.tags | available art width minus shared inset | content-fit within art band | WC2c3.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c3.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c3.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c3.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c3.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c3.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c3.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c3.body.detail1 | 100% usable body width | content-fit within body band | WC2c3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Utility effect / target |
| WC2c3.body.detail2 | 100% usable body width | content-fit within body band | WC2c3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Quantity / availability |
| WC2c3.body.blocker | 100% usable body width | content-fit within body band | WC2c3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c3.selection.outline | 100% card width | 100% card height | WC2c3.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c3.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c3.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Utility effect / target    │
│ Quantity / availability    │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c3.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c3.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c3.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c3.header.title | remaining header width | 100% header height | WC2c3.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c3.header.state | content-fit | 100% header height | WC2c3.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c3.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c3.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c3.art.tags | available art width minus shared inset | content-fit within art band | WC2c3.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c3.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c3.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c3.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c3.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c3.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c3.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c3.body.detail1 | 100% usable body width | content-fit within body band | WC2c3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Utility effect / target |
| WC2c3.body.detail2 | 100% usable body width | content-fit within body band | WC2c3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Quantity / availability |
| WC2c3.body.blocker | 100% usable body width | content-fit within body band | WC2c3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c3.selection.outline | 100% card width | 100% card height | WC2c3.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c3.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c3.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Utility effect / target  │
│ Quantity / availability  │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c3.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c3.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c3.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c3.header.title | remaining header width | 100% header height | WC2c3.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c3.header.state | content-fit | 100% header height | WC2c3.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c3.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c3.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c3.art.tags | available art width minus shared inset | content-fit within art band | WC2c3.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c3.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c3.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c3.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c3.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c3.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c3.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c3.body.detail1 | 100% usable body width | content-fit within body band | WC2c3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Utility effect / target |
| WC2c3.body.detail2 | 100% usable body width | content-fit within body band | WC2c3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Quantity / availability |
| WC2c3.body.blocker | 100% usable body width | content-fit within body band | WC2c3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c3.selection.outline | 100% card width | 100% card height | WC2c3.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c3.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c3.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Utility effect / target  │
│ Quantity / availability  │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC2c3.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC2c3.header | 100% card width | config.cards.geometry.bands.header% of card height | WC2c3.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC2c3.header.title | remaining header width | 100% header height | WC2c3.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC2c3.header.state | content-fit | 100% header height | WC2c3.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC2c3.art | 100% card width | config.cards.geometry.bands.art% of card height | WC2c3.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC2c3.art.tags | available art width minus shared inset | content-fit within art band | WC2c3.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC2c3.body | 100% card width | config.cards.geometry.bands.body% of card height | WC2c3.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC2c3.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC2c3.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC2c3.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC2c3.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC2c3.body.detail1 | 100% usable body width | content-fit within body band | WC2c3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Utility effect / target |
| WC2c3.body.detail2 | 100% usable body width | content-fit within body band | WC2c3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Quantity / availability |
| WC2c3.body.blocker | 100% usable body width | content-fit within body band | WC2c3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC2c3.selection.outline | 100% card width | 100% card height | WC2c3.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC2c3.selection.info | config.components.target.minRem | config.components.target.minRem | WC2c3.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC2c
REQUIRED PROPOSED TAGS: effect-purpose:utility
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC2c3
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC2c3.body.detail1 ← registered provider for Utility effect / target
    WC2c3.body.detail2 ← registered provider for Quantity / availability
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Use") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Targeted use follows existing targeting/confirmation command flow.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC2c3",
  "tags": [
    "effect-purpose:utility"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC2c",
    "name": "Smoke Phial",
    "glyph": "⚱",
    "rules": "Apply the projected evasion effect.",
    "tags": [
      "consumable",
      "utility"
    ],
    "availability": "Requires a valid combat context."
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC3: Character-choice card

**Parent: WC0.** Draft choice only; never commits a new run directly.

**Construction tags (proposed):** `card-kind:creationChoice`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Choice summary                 │
│ Current selection /            │
│ eligibility                    │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC3.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC3.header | 100% card width | config.cards.geometry.bands.header% of card height | WC3.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC3.header.title | remaining header width | 100% header height | WC3.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC3.header.state | content-fit | 100% header height | WC3.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC3.art | 100% card width | config.cards.geometry.bands.art% of card height | WC3.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC3.art.tags | available art width minus shared inset | content-fit within art band | WC3.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC3.body | 100% card width | config.cards.geometry.bands.body% of card height | WC3.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC3.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC3.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC3.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC3.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC3.body.detail1 | 100% usable body width | content-fit within body band | WC3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Choice summary |
| WC3.body.detail2 | 100% usable body width | content-fit within body band | WC3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Current selection / eligibility |
| WC3.body.blocker | 100% usable body width | content-fit within body band | WC3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC3.selection.outline | 100% card width | 100% card height | WC3.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC3.selection.info | config.components.target.minRem | config.components.target.minRem | WC3.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Choice summary             │
│ Current selection /        │
│ eligibility                │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC3.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC3.header | 100% card width | config.cards.geometry.bands.header% of card height | WC3.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC3.header.title | remaining header width | 100% header height | WC3.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC3.header.state | content-fit | 100% header height | WC3.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC3.art | 100% card width | config.cards.geometry.bands.art% of card height | WC3.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC3.art.tags | available art width minus shared inset | content-fit within art band | WC3.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC3.body | 100% card width | config.cards.geometry.bands.body% of card height | WC3.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC3.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC3.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC3.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC3.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC3.body.detail1 | 100% usable body width | content-fit within body band | WC3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Choice summary |
| WC3.body.detail2 | 100% usable body width | content-fit within body band | WC3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Current selection / eligibility |
| WC3.body.blocker | 100% usable body width | content-fit within body band | WC3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC3.selection.outline | 100% card width | 100% card height | WC3.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC3.selection.info | config.components.target.minRem | config.components.target.minRem | WC3.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Choice summary           │
│ Current selection /      │
│ eligibility              │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC3.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC3.header | 100% card width | config.cards.geometry.bands.header% of card height | WC3.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC3.header.title | remaining header width | 100% header height | WC3.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC3.header.state | content-fit | 100% header height | WC3.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC3.art | 100% card width | config.cards.geometry.bands.art% of card height | WC3.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC3.art.tags | available art width minus shared inset | content-fit within art band | WC3.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC3.body | 100% card width | config.cards.geometry.bands.body% of card height | WC3.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC3.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC3.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC3.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC3.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC3.body.detail1 | 100% usable body width | content-fit within body band | WC3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Choice summary |
| WC3.body.detail2 | 100% usable body width | content-fit within body band | WC3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Current selection / eligibility |
| WC3.body.blocker | 100% usable body width | content-fit within body band | WC3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC3.selection.outline | 100% card width | 100% card height | WC3.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC3.selection.info | config.components.target.minRem | config.components.target.minRem | WC3.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Choice summary           │
│ Current selection /      │
│ eligibility              │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC3.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC3.header | 100% card width | config.cards.geometry.bands.header% of card height | WC3.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC3.header.title | remaining header width | 100% header height | WC3.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC3.header.state | content-fit | 100% header height | WC3.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC3.art | 100% card width | config.cards.geometry.bands.art% of card height | WC3.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC3.art.tags | available art width minus shared inset | content-fit within art band | WC3.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC3.body | 100% card width | config.cards.geometry.bands.body% of card height | WC3.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC3.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC3.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC3.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC3.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC3.body.detail1 | 100% usable body width | content-fit within body band | WC3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Choice summary |
| WC3.body.detail2 | 100% usable body width | content-fit within body band | WC3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Current selection / eligibility |
| WC3.body.blocker | 100% usable body width | content-fit within body band | WC3.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC3.selection.outline | 100% card width | 100% card height | WC3.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC3.selection.info | config.components.target.minRem | config.components.target.minRem | WC3.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC0
REQUIRED PROPOSED TAGS: card-kind:creationChoice
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC3
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC3.body.detail1 ← registered provider for Choice summary
    WC3.body.detail2 ← registered provider for Current selection / eligibility
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Choose") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Draft choice only; never commits a new run directly.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC3",
  "tags": [
    "card-kind:creationChoice"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC0",
    "name": "Vanguard",
    "kind": "character-choice",
    "glyph": "♜",
    "headerState": "Class",
    "rules": "A defensive starting archetype.",
    "facts": [
      [
        "Starting focus",
        "Guard"
      ],
      [
        "Resource",
        "Resolve"
      ]
    ],
    "tags": [
      "class"
    ],
    "rarity": "Archetype",
    "owned": null,
    "references": [
      "src/ui/screens/customize.js",
      "src/ui/components/statAllocationCard.js"
    ]
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC3a: Class card

**Parent: WC3.** Selecting updates the draft preview through existing character-creation rules.

**Construction tags (proposed):** `choice-kind:class`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Class identity / role          │
│ Starting stats / abilities     │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC3a.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC3a.header | 100% card width | config.cards.geometry.bands.header% of card height | WC3a.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC3a.header.title | remaining header width | 100% header height | WC3a.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC3a.header.state | content-fit | 100% header height | WC3a.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC3a.art | 100% card width | config.cards.geometry.bands.art% of card height | WC3a.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC3a.art.tags | available art width minus shared inset | content-fit within art band | WC3a.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC3a.body | 100% card width | config.cards.geometry.bands.body% of card height | WC3a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC3a.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC3a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC3a.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC3a.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC3a.body.detail1 | 100% usable body width | content-fit within body band | WC3a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Class identity / role |
| WC3a.body.detail2 | 100% usable body width | content-fit within body band | WC3a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Starting stats / abilities |
| WC3a.body.blocker | 100% usable body width | content-fit within body band | WC3a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC3a.selection.outline | 100% card width | 100% card height | WC3a.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC3a.selection.info | config.components.target.minRem | config.components.target.minRem | WC3a.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Class identity / role      │
│ Starting stats / abilities │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC3a.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC3a.header | 100% card width | config.cards.geometry.bands.header% of card height | WC3a.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC3a.header.title | remaining header width | 100% header height | WC3a.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC3a.header.state | content-fit | 100% header height | WC3a.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC3a.art | 100% card width | config.cards.geometry.bands.art% of card height | WC3a.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC3a.art.tags | available art width minus shared inset | content-fit within art band | WC3a.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC3a.body | 100% card width | config.cards.geometry.bands.body% of card height | WC3a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC3a.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC3a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC3a.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC3a.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC3a.body.detail1 | 100% usable body width | content-fit within body band | WC3a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Class identity / role |
| WC3a.body.detail2 | 100% usable body width | content-fit within body band | WC3a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Starting stats / abilities |
| WC3a.body.blocker | 100% usable body width | content-fit within body band | WC3a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC3a.selection.outline | 100% card width | 100% card height | WC3a.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC3a.selection.info | config.components.target.minRem | config.components.target.minRem | WC3a.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Class identity / role    │
│ Starting stats /         │
│ abilities                │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC3a.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC3a.header | 100% card width | config.cards.geometry.bands.header% of card height | WC3a.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC3a.header.title | remaining header width | 100% header height | WC3a.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC3a.header.state | content-fit | 100% header height | WC3a.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC3a.art | 100% card width | config.cards.geometry.bands.art% of card height | WC3a.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC3a.art.tags | available art width minus shared inset | content-fit within art band | WC3a.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC3a.body | 100% card width | config.cards.geometry.bands.body% of card height | WC3a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC3a.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC3a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC3a.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC3a.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC3a.body.detail1 | 100% usable body width | content-fit within body band | WC3a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Class identity / role |
| WC3a.body.detail2 | 100% usable body width | content-fit within body band | WC3a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Starting stats / abilities |
| WC3a.body.blocker | 100% usable body width | content-fit within body band | WC3a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC3a.selection.outline | 100% card width | 100% card height | WC3a.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC3a.selection.info | config.components.target.minRem | config.components.target.minRem | WC3a.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Class identity / role    │
│ Starting stats /         │
│ abilities                │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC3a.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC3a.header | 100% card width | config.cards.geometry.bands.header% of card height | WC3a.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC3a.header.title | remaining header width | 100% header height | WC3a.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC3a.header.state | content-fit | 100% header height | WC3a.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC3a.art | 100% card width | config.cards.geometry.bands.art% of card height | WC3a.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC3a.art.tags | available art width minus shared inset | content-fit within art band | WC3a.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC3a.body | 100% card width | config.cards.geometry.bands.body% of card height | WC3a.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC3a.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC3a.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC3a.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC3a.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC3a.body.detail1 | 100% usable body width | content-fit within body band | WC3a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Class identity / role |
| WC3a.body.detail2 | 100% usable body width | content-fit within body band | WC3a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Starting stats / abilities |
| WC3a.body.blocker | 100% usable body width | content-fit within body band | WC3a.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC3a.selection.outline | 100% card width | 100% card height | WC3a.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC3a.selection.info | config.components.target.minRem | config.components.target.minRem | WC3a.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC3
REQUIRED PROPOSED TAGS: choice-kind:class
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC3a
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC3a.body.detail1 ← registered provider for Class identity / role
    WC3a.body.detail2 ← registered provider for Starting stats / abilities
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Choose class") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Selecting updates the draft preview through existing character-creation rules.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC3a",
  "tags": [
    "choice-kind:class"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC3",
    "name": "Vanguard"
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC3b: Starting-kit card

**Parent: WC3.** Preserve class eligibility and existing kit definitions; do not duplicate grants into the card model source.

**Construction tags (proposed):** `choice-kind:startingKit`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
               (i)
┌────────────────────────────────┐
│ {Name}                         │
├────────────────────────────────┤
│ [Art / portrait]               │
│                                │
│ {Meaningful tag badges}        │
├────────────────────────────────┤
│ Starting equipment             │
│ Granted playing cards          │
│ {Blocker if needed}            │
├────────────────────────────────┤
│ [   Rarity       Owned: n    ] │
└────────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC3b.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC3b.header | 100% card width | config.cards.geometry.bands.header% of card height | WC3b.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC3b.header.title | remaining header width | 100% header height | WC3b.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC3b.header.state | content-fit | 100% header height | WC3b.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC3b.art | 100% card width | config.cards.geometry.bands.art% of card height | WC3b.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC3b.art.tags | available art width minus shared inset | content-fit within art band | WC3b.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC3b.body | 100% card width | config.cards.geometry.bands.body% of card height | WC3b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC3b.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC3b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC3b.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC3b.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC3b.body.detail1 | 100% usable body width | content-fit within body band | WC3b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Starting equipment |
| WC3b.body.detail2 | 100% usable body width | content-fit within body band | WC3b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Granted playing cards |
| WC3b.body.blocker | 100% usable body width | content-fit within body band | WC3b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC3b.selection.outline | 100% card width | 100% card height | WC3b.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC3b.selection.info | config.components.target.minRem | config.components.target.minRem | WC3b.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Compact**

```text
             (i)
┌────────────────────────────┐
│ {Name}                     │
├────────────────────────────┤
│ [Art / portrait]           │
│                            │
│ {Meaningful tag badges}    │
├────────────────────────────┤
│ Starting equipment         │
│ Granted playing cards      │
│ {Blocker if needed}        │
├────────────────────────────┤
│ [ Rarity       Owned: n  ] │
└────────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC3b.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC3b.header | 100% card width | config.cards.geometry.bands.header% of card height | WC3b.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC3b.header.title | remaining header width | 100% header height | WC3b.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC3b.header.state | content-fit | 100% header height | WC3b.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC3b.art | 100% card width | config.cards.geometry.bands.art% of card height | WC3b.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC3b.art.tags | available art width minus shared inset | content-fit within art band | WC3b.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC3b.body | 100% card width | config.cards.geometry.bands.body% of card height | WC3b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC3b.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC3b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC3b.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC3b.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC3b.body.detail1 | 100% usable body width | content-fit within body band | WC3b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Starting equipment |
| WC3b.body.detail2 | 100% usable body width | content-fit within body band | WC3b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Granted playing cards |
| WC3b.body.blocker | 100% usable body width | content-fit within body band | WC3b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC3b.selection.outline | 100% card width | 100% card height | WC3b.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC3b.selection.info | config.components.target.minRem | config.components.target.minRem | WC3b.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Starting equipment       │
│ Granted playing cards    │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC3b.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC3b.header | 100% card width | config.cards.geometry.bands.header% of card height | WC3b.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC3b.header.title | remaining header width | 100% header height | WC3b.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC3b.header.state | content-fit | 100% header height | WC3b.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC3b.art | 100% card width | config.cards.geometry.bands.art% of card height | WC3b.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC3b.art.tags | available art width minus shared inset | content-fit within art band | WC3b.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC3b.body | 100% card width | config.cards.geometry.bands.body% of card height | WC3b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC3b.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC3b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC3b.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC3b.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC3b.body.detail1 | 100% usable body width | content-fit within body band | WC3b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Starting equipment |
| WC3b.body.detail2 | 100% usable body width | content-fit within body band | WC3b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Granted playing cards |
| WC3b.body.blocker | 100% usable body width | content-fit within body band | WC3b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC3b.selection.outline | 100% card width | 100% card height | WC3b.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC3b.selection.info | config.components.target.minRem | config.components.target.minRem | WC3b.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
            (i)
┌──────────────────────────┐
│ {Name}                   │
├──────────────────────────┤
│ [Art / portrait]         │
│                          │
│ {Meaningful tag badges}  │
├──────────────────────────┤
│ Starting equipment       │
│ Granted playing cards    │
│ {Blocker if needed}      │
├──────────────────────────┤
│ [Rarity       Owned: n ] │
└──────────────────────────┘
```

| Component ID | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Content / ownership |
|---|---|---|---|---|---|---|---|---|
| WC3b.frame | clamp(config.cards.geometry.widthMinimum, config.cards.geometry.widthPreferred, config.cards.geometry.widthMaximum) | resolved width × config.cards.geometry.ratioHeight / config.cards.geometry.ratioWidth | owning hand/grid/picker | host-assigned cell | center / center | host grid item | host gap token | Uniform card envelope |
| WC3b.header | 100% card width | config.cards.geometry.bands.header% of card height | WC3b.frame | top / full width | stretch / center | normal grid flow | 0 | Name left; state right |
| WC3b.header.title | remaining header width | 100% header height | WC3b.header | top-left | start / center | normal grid flow | shared inset token from left; vertically centered in header | Top-left |
| WC3b.header.state | content-fit | 100% header height | WC3b.header | top-right | end / center | normal grid flow | shared inset token from right; vertically centered in header | Top-right if applicable |
| WC3b.art | 100% card width | config.cards.geometry.bands.art% of card height | WC3b.frame | below header | center / center | normal grid flow | 0; preserve intrinsic artwork ratio | Contain artwork; never stretch |
| WC3b.art.tags | available art width minus shared inset | content-fit within art band | WC3b.art | bottom-left | start / center | reserved bottom row inside art band | shared inset token horizontal inset; no extra band height | Meaningful tags bottom-left |
| WC3b.body | 100% card width | config.cards.geometry.bands.body% of card height | WC3b.frame | below preceding band | stretch / stretch | normal grid flow | 0; band padding included in height | Shared fact rows; scroll only when required |
| WC3b.footer | 100% card width | config.cards.geometry.bands.footer% of card height | WC3b.frame | bottom / full width | stretch / center | normal grid flow | 0; reserved grid row | Metadata only |
| WC3b.footer.metadata | available footer width minus shared inset | 100% usable footer height | WC3b.footer | bottom / full usable width | center / center | single-row footer grid item | shared inset token side inset; vertically centered in footer | Rarity left; owned count right |
| WC3b.body.detail1 | 100% usable body width | content-fit within body band | WC3b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Starting equipment |
| WC3b.body.detail2 | 100% usable body width | content-fit within body band | WC3b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Granted playing cards |
| WC3b.body.blocker | 100% usable body width | content-fit within body band | WC3b.body | next row, top to bottom | start / start | normal grid flow | shared inset token horizontal inset; rows share body budget | Omit when absent |
| WC3b.selection.outline | 100% card width | 100% card height | WC3b.frame | perimeter | center / center | anchored overlay following visual card transform | 0; outline outside edge, no layout reflow | Shared owner selection glow |
| WC3b.selection.info | config.components.target.minRem | config.components.target.minRem | WC3b.frame | above top-center | center / center | anchored overlay following visual card transform | 0.75vh gap above lifted card; reserve host headroom | Centered above owner; outside ratio envelope |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: entityRef, instanceSnapshot, context
PARENT: WC3
REQUIRED PROPOSED TAGS: choice-kind:startingKit
model = WC0.ConstructCard(entityRef, instanceSnapshot, context)
REQUIRE compiled family ancestry includes WC3b
// Selection is determined by validated tag rules, not a renderer switch.
AttachFromMatchedRules:
    WC3b.body.detail1 ← registered provider for Starting equipment
    WC3b.body.detail2 ← registered provider for Granted playing cards
InheritParentIdentityArtBadgesPaletteFocusAndActionBehavior()
ResolveFamilyBandOverride(); InheritSelectionAndDelayedInfoInspector()
IF combatant: FilterActiveComponentsThenOrder(HP, resources, buildup, stance, icons)
IF combatant: LimitToFiveRows(); MoveOverflowBuildupIntoIcons(); ReserveFinalPlusNIfNeeded()
IF combatant: ApplyRoleIntentVisibilityAndFacingAnchors(); GlowWholeSelectedAssembly()
owningHost.primaryAction = AvailableContextIntent("Choose kit") OR absent
RenderThroughSharedWC0(model)
ON activation: DelegateToOwningPresenterAndExistingDomainCommand()
ON unavailable: ShowReasonWithoutOfferingAnInvalidCommit()
ON tag/state/context change: ReprojectMatchedSlotsWithoutChangingEntity()
Preserve class eligibility and existing kit definitions; do not duplicate grants into the card model source.
Reuse the same model in wide/compact/portrait; host layout supplies dimensions.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/collectibleCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/collectibleCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/cardSelection.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/cardSelection.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3), [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC3b",
  "tags": [
    "choice-kind:startingKit"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC3",
    "name": "Blade and Guard",
    "glyph": "⚔",
    "headerState": "Kit",
    "rules": "Begin with a blade, armor and guard cards.",
    "facts": [
      [
        "Weapon",
        "Iron blade"
      ],
      [
        "Cards",
        "Strike · Brace"
      ]
    ],
    "tags": [
      "kit"
    ],
    "rarity": "Starting kit"
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "geometry": {
    "ratioWidth": 5,
    "ratioHeight": 8,
    "widthMinimum": "10rem",
    "widthPreferred": "18vw",
    "widthMaximum": "16rem",
    "bands": {
      "header": 10,
      "art": 40,
      "body": 40,
      "footer": 10
    }
  },
  "selection": {
    "delayMs": 1000
  },
  "tooltip": {
    "delayMs": 1000
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "costs": {
    "order": [
      "action",
      "stamina",
      "mana"
    ],
    "showZero": false,
    "railWidthRem": 1.8,
    "insetRem": 0.15,
    "gapRem": 0.1,
    "fontRem": 0.75,
    "iconSizeRem": 0.8,
    "outlineColor": "#120e09",
    "variableLabel": "X",
    "providers": {
      "action": {
        "label": "Actions",
        "glyph": "◆",
        "icon": "diamond",
        "color": "#d1aa60"
      },
      "stamina": {
        "label": "Stamina",
        "glyph": "ϟ",
        "icon": "bolt",
        "color": "#88b96d"
      },
      "mana": {
        "label": "MP",
        "glyph": "♦",
        "icon": "droplet",
        "color": "#79bcec"
      }
    },
    "sampleOverrides": {
      "WC1b": {
        "stamina": 2
      },
      "WC1c": {
        "mana": 3
      }
    }
  },
  "metadata": {
    "ownedLabel": "Owned",
    "unknown": "Unknown",
    "empty": "None"
  },
  "body": {
    "maximumPreviewFacts": 2,
    "showFlavor": false
  },
  "components": {
    "identity": "WCI1",
    "art": "WCI2",
    "footer": "WCI3",
    "inspect": "WCB1",
    "action": "WCB2",
    "facts": "WCF4"
  },
  "features": [
    {
      "tag": "feature:rules",
      "component": "rules",
      "order": 10
    },
    {
      "tag": "feature:facts",
      "component": "facts",
      "order": 20
    },
    {
      "tag": "feature:availability",
      "component": "availability",
      "order": 30
    }
  ],
  "tags": {
    "attack": {
      "label": "Attack",
      "description": "An attack card. Target and effect come from its projected rules."
    },
    "skill": {
      "label": "Skill",
      "description": "A skill card with a context-defined effect."
    },
    "power": {
      "label": "Power",
      "description": "A card whose projected effect can persist."
    },
    "curse": {
      "label": "Curse",
      "description": "An unfavorable card; this fixture cannot be played."
    },
    "status": {
      "label": "Status",
      "description": "A temporary deck condition in this fixture."
    },
    "weapon": {
      "label": "Weapon",
      "description": "Equipment with projected weapon facts and requirements."
    },
    "armor": {
      "label": "Armor",
      "description": "Equipment with projected protection and requirements."
    },
    "relic": {
      "label": "Relic",
      "description": "An owned object with authored passive or triggered effects."
    },
    "passive": {
      "label": "Passive",
      "description": "Applies while its domain activation condition is true."
    },
    "triggered": {
      "label": "Triggered",
      "description": "Applies when its registered trigger resolves."
    },
    "consumable": {
      "label": "Consumable",
      "description": "Using this object consumes its projected charge or quantity."
    },
    "healing": {
      "label": "Healing",
      "description": "Restores health, subject to projected limits."
    },
    "resource": {
      "label": "Resource",
      "description": "Changes the projected resource pool."
    },
    "utility": {
      "label": "Utility",
      "description": "Provides a situational effect."
    },
    "class": {
      "label": "Class",
      "description": "A character archetype and its known starting properties."
    },
    "kit": {
      "label": "Starting kit",
      "description": "A collection of initial equipment and cards."
    },
    "combatant": {
      "label": "Combatant",
      "description": "A borderless actor assembly using WC4 geometry."
    }
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC4: Combatant card

**Parent: WC0.** Borderless combatant presentation: sprite remaining (85% with HP only), resource bars 5% each, statuses 10% of component height; overrides WC0 item-card bands. Three sizes are host presentation variants, never different combatant models. Borderless combatant renderer. Inherits WC0 identity, tag providers, inspection and shared selection effects; overrides item-card bands. Name sits above HP. Intent is above sprite, hidden for player by configurable default; info appears above it after1s. Defense anchors at12% sprite height on player upper-right and88% on enemy lower-left, outside the sprite with0.5rem gap. Aura/buff fill sprite height. Filter active components before stacking HP/resources/buildup/stance/icons; max5 rows, no empty gaps. Extra resource and buildup bars are half HP height; stance matches HP width/height. Icons are1.575rem squares, icon-only; final +N opens the complete inspector. Shared0.2rem stack gap and whole-assembly selected glow. Tooltip delay1s. Inspector uses entity-name title; left sprite/name/HP only; right summary, current state, previous actions, known abilities/traits, lore. Facts are knowledge-filtered and data-driven. Formation fit is shared across factions and measured from unselected geometry. Configured row factors preserve upper/middle/lower depth; selected growth raises the assembly without changing its floor anchor. See CURRENT-SPECIFICATION.md.

**Construction tags (proposed):** `card-kind:combatant`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4.frame | 16rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.

**Compact**

```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4.frame | 16rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4.frame | 16rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4.frame | 16rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: combatant snapshot, player knowledge, role, presentation config, host size
PARENT: WC4 (WC4 inherits WC0 identity and interactions, NOT item-card geometry)
ResolveSizeVariant(WC4); derive height from clamped width, preserve art ratio
intentVisible = domain.intentActive AND config.intentVisibleForRole(role)
// Defaults: player=config.combatant.playerIntentVisible, enemy=config.combatant.enemyIntentVisible; explicit presentation override permitted.
blockVisible = domain.blockActive
resourceRows = ProjectActiveResourceProviders(snapshot)
buildupRows = ProjectActiveBuildupProviders(snapshot)
stance = ProjectActiveStanceOrAbsent(snapshot)
statusIcons = ProjectActiveStatusIcons(snapshot)
ProjectSelectionVisibility(config.visibility.unselectedHiddenSelectors, owner.selected) // Retain HP, block, intent, icons, aura and buffs while unselected.
ReserveHP(); remainingRows = config.combatant.maxStackRows - HP - present(stance) - present(statusIcons OR buildupRows)
ChooseOptionalBarsByConfiguredPriority(resourceRows, buildupRows, remainingRows)
ConvertExcessBuildupToProgressIcons(); retain stable IDs
ComposeActiveRowsInOrder(HP, resources, buildup, stance, icons)
CollapseAbsentRowsAndGaps(); sprite receives remaining height
SetOtherResourceAndBuildupHeight(config.combatant.extraBarHeightRatio * hpHeight); SetStanceHeight(hpHeight)
UseFullBarWidthForStance(); UseSharedGap(config.combatant.stackGapRem)
FitSquareIcons(config.combatant.iconSizeRem); reserve last tile for +N hidden icons if overflow
AnchorNameAboveHP(); IntentAboveSprite(); InfoAboveIntentOrSprite()
AnchorDefenseAtSpriteRatio(config.combatant.defenseAnchorByRole[role], gap=config.combatant.defenseGapRem, player=right, enemy=left)
MirrorArtworkForFacingOnly(); auraBehindAndBuffAboveSpanFullSpriteHeight()
On selected: glow whole visible assembly; reveal info after config.interaction.inspectDelayMs
ApplyRowPresentation(config.combatantFocus.rowBase, config.combatantFocus.selectedGrowth) // Share unselected category fit across factions; selection details never shrink it.
AnchorScaledSpriteAtGroundShadow(); RaiseSelectedTo(config.combatantFocus.focusZ) // Keep foot contact stable and bring selected assembly forward.
On hover/focus/tap tag: schedule shared tooltip after config.interaction.tooltipDelayMs; cancel stale timer
On +N or info: open W1w; preview ONLY sprite/name/HP
Inspector title = entity name
Inspector detail providers: HP/intent/defense, active current state,
 previous actions, known abilities, known traits, lore; apply knowledge filter
Reuse one model across orientations and sizes; no duplicate domain facts
Dispose timers/observers and restore focus on close.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantOverhead.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantOverhead.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/CombatantInspectorModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/CombatantInspectorModel.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCM1](wireframe-gallery.html#WCM1), [WCM2](wireframe-gallery.html#WCM2), [WCM3](wireframe-gallery.html#WCM3), [WCM4](wireframe-gallery.html#WCM4), [WCM5](wireframe-gallery.html#WCM5), [WCM6](wireframe-gallery.html#WCM6), [WCO1](wireframe-gallery.html#WCO1), [WCO2](wireframe-gallery.html#WCO2), [WCO3](wireframe-gallery.html#WCO3), [WCO4](wireframe-gallery.html#WCO4), [WCO5](wireframe-gallery.html#WCO5), [WCB1](wireframe-gallery.html#WCB1), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC4",
  "tags": [
    "card-kind:combatant"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC0",
    "name": "Ashen Sentinel",
    "kind": "combatant",
    "glyph": "♟",
    "headerState": "Enemy",
    "rules": "An illustrative combatant.",
    "facts": [
      [
        "HP",
        "32 / 40"
      ],
      [
        "Defense",
        "8"
      ]
    ],
    "tags": [
      "combatant"
    ],
    "references": [
      "src/ui/components/combatantFrame.js",
      "src/ui/components/combatantInspector.js"
    ],
    "geometry": "WC4 borderless sprite assembly; does not inherit WC0 item face"
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "combatant": {
    "maxStackRows": 5,
    "extraBarHeightRatio": 0.5,
    "stackGapRem": 0.2,
    "iconSizeRem": 1.575,
    "defenseOffsetRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntentVisible": false,
    "enemyIntentVisible": true,
    "legibility": {
      "valueFontMinPx": 12,
      "infoSizePx": 44,
      "infoFontPx": 16,
      "hpMinRem": 0.85,
      "secondaryMinRem": 0.45,
      "stanceMinRem": 0.85,
      "iconMinRem": 1,
      "compactHPFormat": "current/maximum",
      "inspectAnchor": "original-sprite-center"
    }
  },
  "visibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "rowDepth": [
    0.9,
    0.95,
    1
  ],
  "focus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "legibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "geometry": {
    "presentation": "borderless combatant assembly",
    "artwork": "preserve intrinsic ratio; sprite receives remaining height"
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC4a: Compact combatant

**Parent: WC4.** Compact: clamp(10rem, 14vw, 12rem) width; height = width × 8/5. Supplementary full details through inspection. Borderless combatant renderer. Inherits WC0 identity, tag providers, inspection and shared selection effects; overrides item-card bands. Name sits above HP. Intent is above sprite, hidden for player by configurable default; info appears above it after1s. Defense anchors at12% sprite height on player upper-right and88% on enemy lower-left, outside the sprite with0.5rem gap. Aura/buff fill sprite height. Filter active components before stacking HP/resources/buildup/stance/icons; max5 rows, no empty gaps. Extra resource and buildup bars are half HP height; stance matches HP width/height. Icons are1.575rem squares, icon-only; final +N opens the complete inspector. Shared0.2rem stack gap and whole-assembly selected glow. Tooltip delay1s. Inspector uses entity-name title; left sprite/name/HP only; right summary, current state, previous actions, known abilities/traits, lore. Facts are knowledge-filtered and data-driven. Formation fit is shared across factions and measured from unselected geometry. Configured row factors preserve upper/middle/lower depth; selected growth raises the assembly without changing its floor anchor. See CURRENT-SPECIFICATION.md.

**Construction tags (proposed):** `card-kind:combatant`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4a.frame | 12rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4a.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4a.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4a.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4a.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4a.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4a.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4a.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4a.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4a.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4a.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4a.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4a.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4a.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.

**Compact**

```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4a.frame | 12rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4a.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4a.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4a.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4a.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4a.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4a.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4a.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4a.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4a.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4a.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4a.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4a.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4a.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4a.frame | 12rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4a.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4a.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4a.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4a.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4a.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4a.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4a.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4a.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4a.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4a.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4a.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4a.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4a.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4a.frame | 12rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4a.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4a.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4a.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4a.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4a.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4a.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4a.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4a.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4a.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4a.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4a.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4a.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4a.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: combatant snapshot, player knowledge, role, presentation config, host size
PARENT: WC4 (WC4 inherits WC0 identity and interactions, NOT item-card geometry)
ResolveSizeVariant(WC4a); derive height from clamped width, preserve art ratio
intentVisible = domain.intentActive AND config.intentVisibleForRole(role)
// Defaults: player=config.combatant.playerIntentVisible, enemy=config.combatant.enemyIntentVisible; explicit presentation override permitted.
blockVisible = domain.blockActive
resourceRows = ProjectActiveResourceProviders(snapshot)
buildupRows = ProjectActiveBuildupProviders(snapshot)
stance = ProjectActiveStanceOrAbsent(snapshot)
statusIcons = ProjectActiveStatusIcons(snapshot)
ProjectSelectionVisibility(config.visibility.unselectedHiddenSelectors, owner.selected) // Retain HP, block, intent, icons, aura and buffs while unselected.
ReserveHP(); remainingRows = config.combatant.maxStackRows - HP - present(stance) - present(statusIcons OR buildupRows)
ChooseOptionalBarsByConfiguredPriority(resourceRows, buildupRows, remainingRows)
ConvertExcessBuildupToProgressIcons(); retain stable IDs
ComposeActiveRowsInOrder(HP, resources, buildup, stance, icons)
CollapseAbsentRowsAndGaps(); sprite receives remaining height
SetOtherResourceAndBuildupHeight(config.combatant.extraBarHeightRatio * hpHeight); SetStanceHeight(hpHeight)
UseFullBarWidthForStance(); UseSharedGap(config.combatant.stackGapRem)
FitSquareIcons(config.combatant.iconSizeRem); reserve last tile for +N hidden icons if overflow
AnchorNameAboveHP(); IntentAboveSprite(); InfoAboveIntentOrSprite()
AnchorDefenseAtSpriteRatio(config.combatant.defenseAnchorByRole[role], gap=config.combatant.defenseGapRem, player=right, enemy=left)
MirrorArtworkForFacingOnly(); auraBehindAndBuffAboveSpanFullSpriteHeight()
On selected: glow whole visible assembly; reveal info after config.interaction.inspectDelayMs
ApplyRowPresentation(config.combatantFocus.rowBase, config.combatantFocus.selectedGrowth) // Share unselected category fit across factions; selection details never shrink it.
AnchorScaledSpriteAtGroundShadow(); RaiseSelectedTo(config.combatantFocus.focusZ) // Keep foot contact stable and bring selected assembly forward.
On hover/focus/tap tag: schedule shared tooltip after config.interaction.tooltipDelayMs; cancel stale timer
On +N or info: open W1w; preview ONLY sprite/name/HP
Inspector title = entity name
Inspector detail providers: HP/intent/defense, active current state,
 previous actions, known abilities, known traits, lore; apply knowledge filter
Reuse one model across orientations and sizes; no duplicate domain facts
Dispose timers/observers and restore focus on close.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantOverhead.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantOverhead.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/CombatantInspectorModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/CombatantInspectorModel.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCM1](wireframe-gallery.html#WCM1), [WCM2](wireframe-gallery.html#WCM2), [WCM3](wireframe-gallery.html#WCM3), [WCM4](wireframe-gallery.html#WCM4), [WCM5](wireframe-gallery.html#WCM5), [WCM6](wireframe-gallery.html#WCM6), [WCO1](wireframe-gallery.html#WCO1), [WCO2](wireframe-gallery.html#WCO2), [WCO3](wireframe-gallery.html#WCO3), [WCO4](wireframe-gallery.html#WCO4), [WCO5](wireframe-gallery.html#WCO5), [WCB1](wireframe-gallery.html#WCB1), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC4a",
  "tags": [
    "card-kind:combatant"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC4",
    "size": "compact"
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "combatant": {
    "maxStackRows": 5,
    "extraBarHeightRatio": 0.5,
    "stackGapRem": 0.2,
    "iconSizeRem": 1.575,
    "defenseOffsetRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntentVisible": false,
    "enemyIntentVisible": true,
    "legibility": {
      "valueFontMinPx": 12,
      "infoSizePx": 44,
      "infoFontPx": 16,
      "hpMinRem": 0.85,
      "secondaryMinRem": 0.45,
      "stanceMinRem": 0.85,
      "iconMinRem": 1,
      "compactHPFormat": "current/maximum",
      "inspectAnchor": "original-sprite-center"
    }
  },
  "visibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "rowDepth": [
    0.9,
    0.95,
    1
  ],
  "focus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "legibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "geometry": {
    "presentation": "borderless combatant assembly",
    "artwork": "preserve intrinsic ratio; sprite receives remaining height"
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC4b: Standard combatant

**Parent: WC4.** Standard: clamp(12rem, 18vw, 16rem) width; height = width × 8/5. Borderless combatant renderer. Inherits WC0 identity, tag providers, inspection and shared selection effects; overrides item-card bands. Name sits above HP. Intent is above sprite, hidden for player by configurable default; info appears above it after1s. Defense anchors at12% sprite height on player upper-right and88% on enemy lower-left, outside the sprite with0.5rem gap. Aura/buff fill sprite height. Filter active components before stacking HP/resources/buildup/stance/icons; max5 rows, no empty gaps. Extra resource and buildup bars are half HP height; stance matches HP width/height. Icons are1.575rem squares, icon-only; final +N opens the complete inspector. Shared0.2rem stack gap and whole-assembly selected glow. Tooltip delay1s. Inspector uses entity-name title; left sprite/name/HP only; right summary, current state, previous actions, known abilities/traits, lore. Facts are knowledge-filtered and data-driven. Formation fit is shared across factions and measured from unselected geometry. Configured row factors preserve upper/middle/lower depth; selected growth raises the assembly without changing its floor anchor. See CURRENT-SPECIFICATION.md.

**Construction tags (proposed):** `card-kind:combatant`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4b.frame | 16rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4b.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4b.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4b.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4b.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4b.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4b.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4b.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4b.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4b.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4b.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4b.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4b.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4b.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.

**Compact**

```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4b.frame | 16rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4b.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4b.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4b.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4b.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4b.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4b.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4b.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4b.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4b.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4b.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4b.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4b.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4b.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4b.frame | 16rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4b.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4b.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4b.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4b.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4b.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4b.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4b.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4b.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4b.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4b.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4b.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4b.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4b.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4b.frame | 16rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4b.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4b.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4b.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4b.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4b.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4b.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4b.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4b.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4b.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4b.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4b.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4b.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4b.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: combatant snapshot, player knowledge, role, presentation config, host size
PARENT: WC4 (WC4 inherits WC0 identity and interactions, NOT item-card geometry)
ResolveSizeVariant(WC4b); derive height from clamped width, preserve art ratio
intentVisible = domain.intentActive AND config.intentVisibleForRole(role)
// Defaults: player=config.combatant.playerIntentVisible, enemy=config.combatant.enemyIntentVisible; explicit presentation override permitted.
blockVisible = domain.blockActive
resourceRows = ProjectActiveResourceProviders(snapshot)
buildupRows = ProjectActiveBuildupProviders(snapshot)
stance = ProjectActiveStanceOrAbsent(snapshot)
statusIcons = ProjectActiveStatusIcons(snapshot)
ProjectSelectionVisibility(config.visibility.unselectedHiddenSelectors, owner.selected) // Retain HP, block, intent, icons, aura and buffs while unselected.
ReserveHP(); remainingRows = config.combatant.maxStackRows - HP - present(stance) - present(statusIcons OR buildupRows)
ChooseOptionalBarsByConfiguredPriority(resourceRows, buildupRows, remainingRows)
ConvertExcessBuildupToProgressIcons(); retain stable IDs
ComposeActiveRowsInOrder(HP, resources, buildup, stance, icons)
CollapseAbsentRowsAndGaps(); sprite receives remaining height
SetOtherResourceAndBuildupHeight(config.combatant.extraBarHeightRatio * hpHeight); SetStanceHeight(hpHeight)
UseFullBarWidthForStance(); UseSharedGap(config.combatant.stackGapRem)
FitSquareIcons(config.combatant.iconSizeRem); reserve last tile for +N hidden icons if overflow
AnchorNameAboveHP(); IntentAboveSprite(); InfoAboveIntentOrSprite()
AnchorDefenseAtSpriteRatio(config.combatant.defenseAnchorByRole[role], gap=config.combatant.defenseGapRem, player=right, enemy=left)
MirrorArtworkForFacingOnly(); auraBehindAndBuffAboveSpanFullSpriteHeight()
On selected: glow whole visible assembly; reveal info after config.interaction.inspectDelayMs
ApplyRowPresentation(config.combatantFocus.rowBase, config.combatantFocus.selectedGrowth) // Share unselected category fit across factions; selection details never shrink it.
AnchorScaledSpriteAtGroundShadow(); RaiseSelectedTo(config.combatantFocus.focusZ) // Keep foot contact stable and bring selected assembly forward.
On hover/focus/tap tag: schedule shared tooltip after config.interaction.tooltipDelayMs; cancel stale timer
On +N or info: open W1w; preview ONLY sprite/name/HP
Inspector title = entity name
Inspector detail providers: HP/intent/defense, active current state,
 previous actions, known abilities, known traits, lore; apply knowledge filter
Reuse one model across orientations and sizes; no duplicate domain facts
Dispose timers/observers and restore focus on close.
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantOverhead.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantOverhead.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/CombatantInspectorModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/CombatantInspectorModel.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCM1](wireframe-gallery.html#WCM1), [WCM2](wireframe-gallery.html#WCM2), [WCM3](wireframe-gallery.html#WCM3), [WCM4](wireframe-gallery.html#WCM4), [WCM5](wireframe-gallery.html#WCM5), [WCM6](wireframe-gallery.html#WCM6), [WCO1](wireframe-gallery.html#WCO1), [WCO2](wireframe-gallery.html#WCO2), [WCO3](wireframe-gallery.html#WCO3), [WCO4](wireframe-gallery.html#WCO4), [WCO5](wireframe-gallery.html#WCO5), [WCB1](wireframe-gallery.html#WCB1), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC4b",
  "tags": [
    "card-kind:combatant"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC4",
    "size": "standard"
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "combatant": {
    "maxStackRows": 5,
    "extraBarHeightRatio": 0.5,
    "stackGapRem": 0.2,
    "iconSizeRem": 1.575,
    "defenseOffsetRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntentVisible": false,
    "enemyIntentVisible": true,
    "legibility": {
      "valueFontMinPx": 12,
      "infoSizePx": 44,
      "infoFontPx": 16,
      "hpMinRem": 0.85,
      "secondaryMinRem": 0.45,
      "stanceMinRem": 0.85,
      "iconMinRem": 1,
      "compactHPFormat": "current/maximum",
      "inspectAnchor": "original-sprite-center"
    }
  },
  "visibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "rowDepth": [
    0.9,
    0.95,
    1
  ],
  "focus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "legibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "geometry": {
    "presentation": "borderless combatant assembly",
    "artwork": "preserve intrinsic ratio; sprite receives remaining height"
  }
}
```

</details>
<!-- reference-metadata:end -->
### Wireframe WC4c: Expanded combatant

**Parent: WC4.** Expanded: clamp(16rem, 24vw, 20rem) width; height = width × 8/5. No new gameplay facts implied. Borderless combatant renderer. Inherits WC0 identity, tag providers, inspection and shared selection effects; overrides item-card bands. Name sits above HP. Intent is above sprite, hidden for player by configurable default; info appears above it after1s. Defense anchors at12% sprite height on player upper-right and88% on enemy lower-left, outside the sprite with0.5rem gap. Aura/buff fill sprite height. Filter active components before stacking HP/resources/buildup/stance/icons; max5 rows, no empty gaps. Extra resource and buildup bars are half HP height; stance matches HP width/height. Icons are1.575rem squares, icon-only; final +N opens the complete inspector. Shared0.2rem stack gap and whole-assembly selected glow. Tooltip delay1s. Inspector uses entity-name title; left sprite/name/HP only; right summary, current state, previous actions, known abilities/traits, lore. Facts are knowledge-filtered and data-driven. Formation fit is shared across factions and measured from unselected geometry. Configured row factors preserve upper/middle/lower depth; selected growth raises the assembly without changing its floor anchor. See CURRENT-SPECIFICATION.md.

**Construction tags (proposed):** `card-kind:combatant`. Inherit ancestor tag requirements; compatible feature tags attach additional components.

**Wide**

```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4c.frame | 20rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4c.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4c.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4c.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4c.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4c.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4c.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4c.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4c.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4c.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4c.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4c.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4c.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4c.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.

**Compact**

```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4c.frame | 20rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4c.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4c.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4c.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4c.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4c.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4c.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4c.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4c.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4c.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4c.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4c.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4c.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4c.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4c.frame | 20rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4c.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4c.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4c.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4c.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4c.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4c.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4c.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4c.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4c.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4c.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4c.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4c.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4c.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
             (i)
        [INTENT: ATTACK]
     {Aura behind sprite}
       [Combatant sprite]
      {Buff overlay}
    [DEFENSE at sprite 50%]
       {Combatant name}

       Sprite: remaining H

     [HP =================] 5% H
     [Resource ===========] 5% H each
     [Buildup ============]
     [ Stance             ]
       [◆] [☠] [✚]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WC4c.frame | 20rem max; host-clamped | derived from shared combatant envelope | battlefield slot | bottom-center | center / end | normal flow | shared stage baseline | transparent; no border or item-card furniture |
| WC4c.name | 100% component | auto; readable line | frame | sprite bottom-center, above HP | center / center | inside sprite bottom; reserve text room | 0.2rem gap | entity display name; reserve host headroom |
| WC4c.intent | content-fit | readable line | frame | above sprite, below info | center / center | leading slot | shared gap | existing announced intent; tooltip; player hidden/enemy visible by configurable default |
| WC4c.aura | sprite bounds | sprite bounds | sprite | center | center / center | behind sprite layer | 0 | decorative, no pointer capture |
| WC4c.buffLayer | sprite width minus shared horizontal inset | 100% sprite height | sprite | center | center / center | foreground effect layer | 0 | decorative; semantic buff remains in status model |
| WC4c.defense | 3.5rem minimum | 3.5rem minimum | sprite | player upper-right at 12%; enemy lower-left at 88% sprite height | center / center | badge overlay | 0.5rem outside sprite; side follows facing | current block/defense model |
| WC4c.sprite | 100% component | H minus bars and statuses | frame | top-center | center / end | row 1 | 0 | contain sprite; preserve intrinsic art ratio |
| WC4c.resources | 100% component | HP height + 0.5 × HP height per extra resource | frame | below sprite | center / center | row 2; stacked meters | shared bar gap included | HP always; applicable extra resources from model |
| WC4c.resources.bar | 100% component | HP: base height; other resources: half HP height | resources | next row | center / center | normal flow | shared gap | label + current/max; semantic color; never color-only |
| WC4c.status | 100% component | content-fit; sprite yields space | frame | below resources | center / start | row 3 | 0.2rem shared gap | contains buildup, stance, icons in this order |
| WC4c.buildup | 100% component | 0.5 × HP bar height | status | top | stretch / center | rows first | config.combatant.stackGapRem | threshold progress; semantic color and text |
| WC4c.statusIcons | 100% component | 1.575rem; exactly one icon row | status | below stance | center / center | last row; final +N opens inspector | config.combatant.stackGapRem | uniform 1.575rem square tiles; icon only; details in tooltip/inspector |
| WC4c.stance | 100% component | 1 × HP bar height | status | below buildup | center / center | before status icons | config.combatant.stackGapRem | uniform badge dimensions across all stances |
| WC4c.info | minimum input target | minimum input target | frame | above top-center | center / center | overlay | above intent with shared gap | outside H; selection delay inherited |

H is component height, not screen height. The sprite receives remaining height after measuring the active lower stack; no viewport-fraction conversion may distort its envelope. Minimum readable bars override nominal allocation; host scales uniformly or uses inspection if space runs out. Presentation variants: player faces right, enemy faces left; mirror only sprite artwork, never labels/meters. Defense stays outside the sprite with0.5rem gap on the facing side: player right, enemy left, at12% sprite height for player and88% for enemy. Selected variant adds shared targeting emphasis and delayed info. Aura is visibly present at sprite top edge. Filter inactive optional components before layout; collapse omitted rows and gaps. Preserve HP → resources → buildup → stance → icons order among active components. Block/intent overlays are also active-only, subject to role visibility. Maximum five lower rows: reserve HP, stance and icons; at most two extra resource/buildup bars. Excess buildup moves into progress icons. If icons exceed width, reserve last slot for +N hidden items; inspector shows full list. Preserve existing leading intentions, block badge and stage baseline as overlays/registered slots; no gameplay change.

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: combatant snapshot, player knowledge, role, presentation config, host size
PARENT: WC4 (WC4 inherits WC0 identity and interactions, NOT item-card geometry)
ResolveSizeVariant(WC4c); derive height from clamped width, preserve art ratio
intentVisible = domain.intentActive AND config.intentVisibleForRole(role)
// Defaults: player=config.combatant.playerIntentVisible, enemy=config.combatant.enemyIntentVisible; explicit presentation override permitted.
blockVisible = domain.blockActive
resourceRows = ProjectActiveResourceProviders(snapshot)
buildupRows = ProjectActiveBuildupProviders(snapshot)
stance = ProjectActiveStanceOrAbsent(snapshot)
statusIcons = ProjectActiveStatusIcons(snapshot)
ProjectSelectionVisibility(config.visibility.unselectedHiddenSelectors, owner.selected) // Retain HP, block, intent, icons, aura and buffs while unselected.
ReserveHP(); remainingRows = config.combatant.maxStackRows - HP - present(stance) - present(statusIcons OR buildupRows)
ChooseOptionalBarsByConfiguredPriority(resourceRows, buildupRows, remainingRows)
ConvertExcessBuildupToProgressIcons(); retain stable IDs
ComposeActiveRowsInOrder(HP, resources, buildup, stance, icons)
CollapseAbsentRowsAndGaps(); sprite receives remaining height
SetOtherResourceAndBuildupHeight(config.combatant.extraBarHeightRatio * hpHeight); SetStanceHeight(hpHeight)
UseFullBarWidthForStance(); UseSharedGap(config.combatant.stackGapRem)
FitSquareIcons(config.combatant.iconSizeRem); reserve last tile for +N hidden icons if overflow
AnchorNameAboveHP(); IntentAboveSprite(); InfoAboveIntentOrSprite()
AnchorDefenseAtSpriteRatio(config.combatant.defenseAnchorByRole[role], gap=config.combatant.defenseGapRem, player=right, enemy=left)
MirrorArtworkForFacingOnly(); auraBehindAndBuffAboveSpanFullSpriteHeight()
On selected: glow whole visible assembly; reveal info after config.interaction.inspectDelayMs
ApplyRowPresentation(config.combatantFocus.rowBase, config.combatantFocus.selectedGrowth) // Share unselected category fit across factions; selection details never shrink it.
AnchorScaledSpriteAtGroundShadow(); RaiseSelectedTo(config.combatantFocus.focusZ) // Keep foot contact stable and bring selected assembly forward.
On hover/focus/tap tag: schedule shared tooltip after config.interaction.tooltipDelayMs; cancel stale timer
On +N or info: open W1w; preview ONLY sprite/name/HP
Inspector title = entity name
Inspector detail providers: HP/intent/defense, active current state,
 previous actions, known abilities, known traits, lore; apply knowledge filter
Reuse one model across orientations and sizes; no duplicate domain facts
Dispose timers/observers and restore focus on close.
```

## Verification

Validate all three modes, inherited construction, multi-tag combinations, missing/unknown tags, optional components, state transitions, scoped entity IDs, readable artwork/text, inline actions and minimum targets. Adding a supported item/class/relic variation should require only normalized data/tag rows, with no new renderer branch. New behavior requires a registered tested primitive first.

# Tooltip wireframes

Three configurable sizes: compact, standard and expanded. WT0 is an anchored overlay, not a modal: no close/back/primary footer. It inherits palette, spacing, typography and reduced-motion tokens from W0. Tooltips contain descriptive text only; interactive or lengthy detail opens W1 inspection.


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantOverhead.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantOverhead.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/CombatantInspectorModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/CombatantInspectorModel.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCM1](wireframe-gallery.html#WCM1), [WCM2](wireframe-gallery.html#WCM2), [WCM3](wireframe-gallery.html#WCM3), [WCM4](wireframe-gallery.html#WCM4), [WCM5](wireframe-gallery.html#WCM5), [WCM6](wireframe-gallery.html#WCM6), [WCO1](wireframe-gallery.html#WCO1), [WCO2](wireframe-gallery.html#WCO2), [WCO3](wireframe-gallery.html#WCO3), [WCO4](wireframe-gallery.html#WCO4), [WCO5](wireframe-gallery.html#WCO5), [WCB1](wireframe-gallery.html#WCB1), [WCF3](wireframe-gallery.html#WCF3), [W1w](wireframe-gallery.html#W1w)

**Actual reference code:** [wireframe-gallery-template.html](wireframe-gallery-template.html), [card-reference-client.js](card-reference-client.js), [card-reference.mjs](card-reference.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `card`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [wireframe-gallery-template.html#style](wireframe-gallery-template.html#style), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WC4c",
  "tags": [
    "card-kind:combatant"
  ],
  "ownerState": {
    "selected": false
  },
  "registryRecord": {
    "parent": "WC4",
    "size": "expanded"
  }
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "combatant": {
    "maxStackRows": 5,
    "extraBarHeightRatio": 0.5,
    "stackGapRem": 0.2,
    "iconSizeRem": 1.575,
    "defenseOffsetRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntentVisible": false,
    "enemyIntentVisible": true,
    "legibility": {
      "valueFontMinPx": 12,
      "infoSizePx": 44,
      "infoFontPx": 16,
      "hpMinRem": 0.85,
      "secondaryMinRem": 0.45,
      "stanceMinRem": 0.85,
      "iconMinRem": 1,
      "compactHPFormat": "current/maximum",
      "inspectAnchor": "original-sprite-center"
    }
  },
  "visibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "rowDepth": [
    0.9,
    0.95,
    1
  ],
  "focus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "legibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "geometry": {
    "presentation": "borderless combatant assembly",
    "artwork": "preserve intrinsic ratio; sprite receives remaining height"
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WT0: Shared tooltip

**Parent: shared overlay primitives.** Anchor to trigger top-center with a 0.5rem gap; flip below when above cannot fit, shift horizontally inside safe viewport. Arrow points to the trigger after shifting. Keep tooltip open while trigger or tooltip is hovered; keyboard focus and tap reveal it; Escape and outside tap dismiss.

**Wide**

```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT0.frame | 20vw nominal | auto; 12vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT0.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT0.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT0.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |

**Compact**

```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT0.frame | 30vw nominal | auto; 12vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT0.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT0.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT0.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT0.frame | 60vw nominal | auto; 12vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT0.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT0.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT0.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT0.frame | 60vw nominal | auto; 12vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT0.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT0.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT0.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
ResolveTooltipModelFromTagRegistry(trigger.tagId, locale)
ChooseConfiguredSize(WT0)
MeasureContentWithinClampedWidth()
PlaceAboveTrigger(); if overflow: FlipBelow(); ShiftInsideSafeViewport()
PointArrowAtTrigger(); renderInActiveModalOverlayOrRoot()
On hover/focus/tap: schedule after config.delayMs=config.referenceTokens.value_1000.value; cancel on leave/blur/removal; on Escape/outside tap: dismiss
KeepOpenAcrossTriggerToTooltipPointerTransition()
CancelOnTriggerRemoval(); never execute domain commands
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/tooltip.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/tooltip.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "context": "WT0"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WT1: Compact tooltip

**Parent: WT0.** Anchor to trigger top-center with a 0.5rem gap; flip below when above cannot fit, shift horizontally inside safe viewport. Arrow points to the trigger after shifting. Keep tooltip open while trigger or tooltip is hovered; keyboard focus and tap reveal it; Escape and outside tap dismiss.

**Wide**

```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT1.frame | 12vw nominal | auto; 6vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT1.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT1.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT1.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |

**Compact**

```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT1.frame | 18vw nominal | auto; 6vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT1.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT1.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT1.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT1.frame | 36vw nominal | auto; 6vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT1.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT1.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT1.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT1.frame | 36vw nominal | auto; 6vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT1.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT1.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT1.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
ResolveTooltipModelFromTagRegistry(trigger.tagId, locale)
ChooseConfiguredSize(WT1)
MeasureContentWithinClampedWidth()
PlaceAboveTrigger(); if overflow: FlipBelow(); ShiftInsideSafeViewport()
PointArrowAtTrigger(); renderInActiveModalOverlayOrRoot()
On hover/focus/tap: schedule after config.delayMs=config.referenceTokens.value_1000.value; cancel on leave/blur/removal; on Escape/outside tap: dismiss
KeepOpenAcrossTriggerToTooltipPointerTransition()
CancelOnTriggerRemoval(); never execute domain commands
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/tooltip.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/tooltip.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "context": "WT1"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WT2: Standard tooltip

**Parent: WT0.** Anchor to trigger top-center with a 0.5rem gap; flip below when above cannot fit, shift horizontally inside safe viewport. Arrow points to the trigger after shifting. Keep tooltip open while trigger or tooltip is hovered; keyboard focus and tap reveal it; Escape and outside tap dismiss.

**Wide**

```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT2.frame | 20vw nominal | auto; 12vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT2.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT2.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT2.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |

**Compact**

```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT2.frame | 30vw nominal | auto; 12vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT2.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT2.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT2.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT2.frame | 60vw nominal | auto; 12vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT2.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT2.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT2.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT2.frame | 60vw nominal | auto; 12vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT2.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT2.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT2.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
ResolveTooltipModelFromTagRegistry(trigger.tagId, locale)
ChooseConfiguredSize(WT2)
MeasureContentWithinClampedWidth()
PlaceAboveTrigger(); if overflow: FlipBelow(); ShiftInsideSafeViewport()
PointArrowAtTrigger(); renderInActiveModalOverlayOrRoot()
On hover/focus/tap: schedule after config.delayMs=config.referenceTokens.value_1000.value; cancel on leave/blur/removal; on Escape/outside tap: dismiss
KeepOpenAcrossTriggerToTooltipPointerTransition()
CancelOnTriggerRemoval(); never execute domain commands
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/tooltip.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/tooltip.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "context": "WT2"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WT3: Expanded tooltip

**Parent: WT0.** Anchor to trigger top-center with a 0.5rem gap; flip below when above cannot fit, shift horizontally inside safe viewport. Arrow points to the trigger after shifting. Keep tooltip open while trigger or tooltip is hovered; keyboard focus and tap reveal it; Escape and outside tap dismiss.

**Wide**

```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT3.frame | 28vw nominal | auto; 20vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT3.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT3.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT3.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |

**Compact**

```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT3.frame | 42vw nominal | auto; 20vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT3.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT3.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT3.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT3.frame | 84vw nominal | auto; 20vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT3.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT3.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT3.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌────────────────────────────┐
│ {Tag / semantic title}     │
│ {Concise description}      │
└─────────────┬──────────────┘
              ▽
          [Trigger]
```

| Component | Width | Height | Relative to | Anchor | Align X / Y | Positioning | Offset / gap | Rule |
|---|---|---|---|---|---|---|---|---|
| WT3.frame | 84vw nominal | auto; 20vh reference | trigger | top-center; flip below | center / end | fixed overlay | 0.5rem gap | clamp to safe viewport; content height, never clip text |
| WT3.title | parent minus 1.5rem | auto | frame | top-left | start / start | normal flow | 0.75rem inset | optional, omit repeated title |
| WT3.body | parent minus 1.5rem | auto | frame | below title | start / start | normal flow | 0.375rem gap | readable text wins |
| WT3.arrow | 0.75rem | 0.375rem | frame | nearest trigger edge | center / center | overlay | tracks trigger center | decorative |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
ResolveTooltipModelFromTagRegistry(trigger.tagId, locale)
ChooseConfiguredSize(WT3)
MeasureContentWithinClampedWidth()
PlaceAboveTrigger(); if overflow: FlipBelow(); ShiftInsideSafeViewport()
PointArrowAtTrigger(); renderInActiveModalOverlayOrRoot()
On hover/focus/tap: schedule after config.delayMs=config.referenceTokens.value_1000.value; cancel on leave/blur/removal; on Escape/outside tap: dismiss
KeepOpenAcrossTriggerToTooltipPointerTransition()
CancelOnTriggerRemoval(); never execute domain commands
```


# Shared component wireframes

WP identifiers describe composable parts, not additional screen families. Every part receives an immutable view model and owner interaction state. WC0/WC4 and W1w compose these same components; context controls visibility and placement. Dimensions are shared configurable proposals.


<!-- reference-metadata:start -->
**Source description and inheritance**

Executable documentation with illustrative records; no domain or persistence mutation.

- [src/ui/components/tooltip.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/tooltip.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [screen-reference-client.js](screen-reference-client.js), [screen-reference.mjs](screen-reference.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderScreenReference`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [screen-reference.css](screen-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "context": "WT3"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "inset": "1rem",
  "gap": ".5rem",
  "sidebarShare": "26%",
  "minimumTarget": "2.75rem",
  "bodyHeight": "26rem",
  "portraitBodyHeight": "25rem",
  "previewCardWidth": "12rem",
  "controls": {
    "rangeMin": 0,
    "rangeMax": 150,
    "rangeStep": 5,
    "attributeMin": 0,
    "attributeMax": 20,
    "attributeStep": 1,
    "attributeBudget": 3
  },
  "headerShare": 10,
  "bodyShare": 80,
  "footerShare": 10
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe W1x: Proficiencies

**Parent: W1.** Use cases: Character profile and post-combat progression review. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
┌──────────────────────────────────────────────────────────────────────────┐
│ Proficiencies                                                        [×] │
├──────────────────┬──────────────────┬────────────────────────────────────┤
│ [    Weapons   ] │ ┌──────────────┐ │ Swords                      Rank 3 │
│ [    Skills    ] │ │ Swords     3 │ │ [████████░░░░░░░░░░░░░░]    42/100 │
│                  │ │ [██░] 42/100 │ │ Next benefit                       │
│                  │ └──────────────┘ │ Known techniques                   │
│                  │ ┌──────────────┐ │ Recent practice                    │
│                  │ │ Bows       2 │ │                                    │
│                  │ │ [██░] 68/100 │ │                                    │
│                  │ └──────────────┘ │                                    │
├──────────────────┴──────────────────┴────────────────────────────────────┤
│ [                                 Back                                 ] │
└──────────────────────────────────────────────────────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| W1x.root | config.progression.layout.width | config.progression.layout.height | owning component slot | W0 title/exit/footer anchors | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
┌───────────────────────────┐
│ Proficiencies          [×] │
│ [Weapons ▾]  [Swords ▾]   │
├───────────────────────────┤
│ Swords            Rank 3  │
│ [████░░░░░░]      42/100  │
│ Next benefit              │
│ Known techniques          │
│ Recent practice           │
├───────────────────────────┤
│ [         Back          ] │
└───────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| W1x.root | config.progression.layout.width | config.progression.layout.height | owning component slot | W0 title/exit/footer anchors | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Proficiencies          [×] │
│ [Weapons ▾]  [Swords ▾]   │
├───────────────────────────┤
│ Swords            Rank 3  │
│ [████░░░░░░]      42/100  │
│ Next benefit              │
│ Known techniques          │
│ Recent practice           │
├───────────────────────────┤
│ [         Back          ] │
└───────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| W1x.root | config.progression.layout.width | config.progression.layout.height | owning component slot | W0 title/exit/footer anchors | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Proficiencies          [×] │
│ [Weapons ▾]  [Swords ▾]   │
├───────────────────────────┤
│ Swords            Rank 3  │
│ [████░░░░░░]      42/100  │
│ Next benefit              │
│ Known techniques          │
│ Recent practice           │
├───────────────────────────┤
│ [         Back          ] │
└───────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| W1x.root | config.progression.layout.width | config.progression.layout.height | owning component slot | W0 title/exit/footer anchors | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT character knowledge, progression projection, config.progression
// Category and proficiency selection are presentation state, not progression mutations.
category = ResolveCategoryOrDefault(config.progression.categories, config.progression.defaultCategory)
entries = ProjectKnownProficiencies(character, category)
selected = ResolveStableSelection(entries)
ComposeW1Shell(title=config.progression.labels.title)
// Columns share a top anchor; category buttons and tiles never stretch to fill height.
// Shell inset alone defines outer left/right anchors; add gutters only at internal dividers.
// Compensate column tracks for unequal gutter counts so usable button widths match.
AlignDetailHeadingsMetersAndTextToOneInlineStart()
SetCategoryButtonHeight(config.progression.layout.standardButtonHeight)
SetTileHeight(config.progression.layout.standardButtonHeight * config.progression.layout.tileHeightMultiplier)
SetColumnTracks(config.progression.layout.sidebar, config.progression.layout.list, config.progression.layout.detail)
// Category and tile tracks have equal width and equal horizontal insets.
UseUniformWidthWithinEachColumn(); UseSharedColumnInset(config.progression.layout.inset)
AlignColumnsToTop(); SeparateColumnsWithVerticalRules()
Compose(WGP1, entries); Compose(WGP2, selected); Compose(WGP3, selected); Compose(WGP4, selected)
// Compact hosts replace navigation columns with dropdowns; retain the same selected ID.
AdaptNavigationToHost(config.progression.layout.compactBreakpoint)
RenderOnlyKnownBenefitsAndTechniques(character.knowledge)
If config.progression.showHistory: RenderOrderedPracticeEvents(selected)
// Back is the sole production footer command and therefore fills the footer.
Compose(WCB4, width=AvailableFooterWidth())
// Preview award is outside the game frame and changes only cloned fixture state.
If config.progression.allowPreviewAward: ExposePreviewAward(config.progression.previewAward)
// Each viewport gets isolated preview state; controls stay outside the game frame.
instance = ClonePresentationState(config.progression)
ExposeInstanceControls(instance.showLockedTechniques, instance.showHistory, instance.previewAward)
OnPreviewControlChange: ReprojectOnlyThisInstance(instance)
OnResetPreview: RestoreThisInstanceDefaults(config.progression)
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

The existing equipment screen, equipment cards, modal shell and meters are source references for presentation reuse. Per-weapon and per-skill practice progression is proposed; these sample ranks, names and awards are illustrative, not shipped mechanics.

- [src/ui/screens/equipment.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/equipment.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/resbars.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/resbars.js#L1) — Current owner checkout (including inspected local edits)
- [PROGRESSION-SPECIFICATION.md](PROGRESSION-SPECIFICATION.md) — Proposed reference specification

**Referenced components:** [WGP1](wireframe-gallery.html#WGP1), [WGP2](wireframe-gallery.html#WGP2), [WGP3](wireframe-gallery.html#WGP3), [WGP4](wireframe-gallery.html#WGP4), [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [progression-reference-client.js](progression-reference-client.js), [progression-reference.mjs](progression-reference.mjs)

Renderer binding: `renderProgression`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [progression-reference.css](progression-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
[
  {
    "id": "swords",
    "category": "weapons",
    "name": "Swords",
    "rank": 3,
    "progress": 42,
    "required": 100,
    "description": "Practice with sword techniques.",
    "next": "Next sword technique becomes available for review.",
    "techniques": [
      {
        "name": "Measured cut",
        "known": true
      },
      {
        "name": "Guard break",
        "known": false
      }
    ],
    "history": [
      "Last encounter: sword practice recorded."
    ]
  },
  {
    "id": "bows",
    "category": "weapons",
    "name": "Bows",
    "rank": 2,
    "progress": 68,
    "required": 100,
    "description": "Practice with aimed attacks.",
    "next": "Next bow technique becomes available for review.",
    "techniques": [
      {
        "name": "Steady aim",
        "known": true
      },
      {
        "name": "Piercing shot",
        "known": false
      }
    ],
    "history": [
      "Last encounter: bow practice recorded."
    ]
  },
  {
    "id": "restoration",
    "category": "skills",
    "name": "Restoration",
    "rank": 2,
    "progress": 35,
    "required": 100,
    "description": "Practice with restorative skills.",
    "next": "Next restoration technique becomes available for review.",
    "techniques": [
      {
        "name": "Mend",
        "known": true
      },
      {
        "name": "Renew",
        "known": false
      }
    ],
    "history": [
      "Last encounter: restoration practice recorded."
    ]
  },
  {
    "id": "guarding",
    "category": "skills",
    "name": "Guarding",
    "rank": 1,
    "progress": 80,
    "required": 100,
    "description": "Practice with defensive skills.",
    "next": "Next defensive technique becomes available for review.",
    "techniques": [
      {
        "name": "Brace",
        "known": true
      },
      {
        "name": "Counterguard",
        "known": false
      }
    ],
    "history": [
      "Last encounter: defensive practice recorded."
    ]
  }
]
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "categories": [
    "weapons",
    "skills"
  ],
  "categoryIds": {
    "weapons": "weapons",
    "skills": "skills"
  },
  "defaultCategory": "weapons",
  "showLockedTechniques": true,
  "showHistory": true,
  "allowPreviewAward": true,
  "previewAward": 12,
  "progressMinimum": 0,
  "progressMaximum": 100,
  "labels": {
    "title": "Proficiencies",
    "back": "Back",
    "award": "Preview progress"
  },
  "layout": {
    "width": "min(95vw, 68rem)",
    "height": "min(90vh, 48rem)",
    "inset": "1rem",
    "gap": ".75rem",
    "sidebar": "minmax(0, 1fr)",
    "list": "minmax(0, 1fr)",
    "detail": "minmax(0, 2fr)",
    "meterHeight": ".6rem",
    "compactBreakpoint": "42rem",
    "standardButtonHeight": "2.75rem",
    "tileHeightMultiplier": 2,
    "categoryButtonSize": "full-standard",
    "tileButtonSize": "full-double"
  },
  "colors": {
    "background": "#241c13",
    "inset": "#19140e",
    "text": "#efe5d4",
    "muted": "#c1ad8b",
    "gold": "#d1aa60",
    "progress": "#4189b6",
    "primary": "#557b3f",
    "danger": "#ac4e3c"
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe W1x1: Weapon proficiency

**Parent: W1x.** Use cases: Weapons category; same workspace model. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
┌──────────────────────────────────────────────────────────────────────────┐
│ Proficiencies                                                        [×] │
├──────────────────┬──────────────────┬────────────────────────────────────┤
│ [    Weapons   ] │ ┌──────────────┐ │ Swords                      Rank 3 │
│ [    Skills    ] │ │ Swords     3 │ │ [████████░░░░░░░░░░░░░░]    42/100 │
│                  │ │ [██░] 42/100 │ │ Next benefit                       │
│                  │ └──────────────┘ │ Known techniques                   │
│                  │ ┌──────────────┐ │ Recent practice                    │
│                  │ │ Bows       2 │ │                                    │
│                  │ │ [██░] 68/100 │ │                                    │
│                  │ └──────────────┘ │                                    │
├──────────────────┴──────────────────┴────────────────────────────────────┤
│ [                                 Back                                 ] │
└──────────────────────────────────────────────────────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| W1x1.root | parent body width | parent body height | owning component slot | W1x active body | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
┌───────────────────────────┐
│ Proficiencies          [×] │
│ [Weapons ▾]  [Swords ▾]   │
├───────────────────────────┤
│ Swords            Rank 3  │
│ [████░░░░░░]      42/100  │
│ Next benefit              │
│ Known techniques          │
│ Recent practice           │
├───────────────────────────┤
│ [         Back          ] │
└───────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| W1x1.root | parent body width | parent body height | owning component slot | W1x active body | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Proficiencies          [×] │
│ [Weapons ▾]  [Swords ▾]   │
├───────────────────────────┤
│ Swords            Rank 3  │
│ [████░░░░░░]      42/100  │
│ Next benefit              │
│ Known techniques          │
│ Recent practice           │
├───────────────────────────┤
│ [         Back          ] │
└───────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| W1x1.root | parent body width | parent body height | owning component slot | W1x active body | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Proficiencies          [×] │
│ [Weapons ▾]  [Swords ▾]   │
├───────────────────────────┤
│ Swords            Rank 3  │
│ [████░░░░░░]      42/100  │
│ Next benefit              │
│ Known techniques          │
│ Recent practice           │
├───────────────────────────┤
│ [         Back          ] │
└───────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| W1x1.root | parent body width | parent body height | owning component slot | W1x active body | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT character knowledge, progression projection, config.progression
// Category and proficiency selection are presentation state, not progression mutations.
category = ResolveCategoryOrDefault(config.progression.categories, config.progression.defaultCategory)
entries = ProjectKnownProficiencies(character, category)
selected = ResolveStableSelection(entries)
ComposeW1Shell(title=config.progression.labels.title)
// Columns share a top anchor; category buttons and tiles never stretch to fill height.
// Shell inset alone defines outer left/right anchors; add gutters only at internal dividers.
// Compensate column tracks for unequal gutter counts so usable button widths match.
AlignDetailHeadingsMetersAndTextToOneInlineStart()
SetCategoryButtonHeight(config.progression.layout.standardButtonHeight)
SetTileHeight(config.progression.layout.standardButtonHeight * config.progression.layout.tileHeightMultiplier)
SetColumnTracks(config.progression.layout.sidebar, config.progression.layout.list, config.progression.layout.detail)
// Category and tile tracks have equal width and equal horizontal insets.
UseUniformWidthWithinEachColumn(); UseSharedColumnInset(config.progression.layout.inset)
AlignColumnsToTop(); SeparateColumnsWithVerticalRules()
Compose(WGP1, entries); Compose(WGP2, selected); Compose(WGP3, selected); Compose(WGP4, selected)
// Compact hosts replace navigation columns with dropdowns; retain the same selected ID.
AdaptNavigationToHost(config.progression.layout.compactBreakpoint)
RenderOnlyKnownBenefitsAndTechniques(character.knowledge)
If config.progression.showHistory: RenderOrderedPracticeEvents(selected)
// Back is the sole production footer command and therefore fills the footer.
Compose(WCB4, width=AvailableFooterWidth())
// Preview award is outside the game frame and changes only cloned fixture state.
If config.progression.allowPreviewAward: ExposePreviewAward(config.progression.previewAward)
// Each viewport gets isolated preview state; controls stay outside the game frame.
instance = ClonePresentationState(config.progression)
ExposeInstanceControls(instance.showLockedTechniques, instance.showHistory, instance.previewAward)
OnPreviewControlChange: ReprojectOnlyThisInstance(instance)
OnResetPreview: RestoreThisInstanceDefaults(config.progression)
SelectCategory(config.progression.categoryIds.weapons) // Bind configured category ID, not a mechanics rule.
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

The existing equipment screen, equipment cards, modal shell and meters are source references for presentation reuse. Per-weapon and per-skill practice progression is proposed; these sample ranks, names and awards are illustrative, not shipped mechanics.

- [src/ui/screens/equipment.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/equipment.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/resbars.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/resbars.js#L1) — Current owner checkout (including inspected local edits)
- [PROGRESSION-SPECIFICATION.md](PROGRESSION-SPECIFICATION.md) — Proposed reference specification

**Referenced components:** [WGP1](wireframe-gallery.html#WGP1), [WGP2](wireframe-gallery.html#WGP2), [WGP3](wireframe-gallery.html#WGP3), [WGP4](wireframe-gallery.html#WGP4), [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [progression-reference-client.js](progression-reference-client.js), [progression-reference.mjs](progression-reference.mjs)

Renderer binding: `renderProgression`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [progression-reference.css](progression-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
[
  {
    "id": "swords",
    "category": "weapons",
    "name": "Swords",
    "rank": 3,
    "progress": 42,
    "required": 100,
    "description": "Practice with sword techniques.",
    "next": "Next sword technique becomes available for review.",
    "techniques": [
      {
        "name": "Measured cut",
        "known": true
      },
      {
        "name": "Guard break",
        "known": false
      }
    ],
    "history": [
      "Last encounter: sword practice recorded."
    ]
  },
  {
    "id": "bows",
    "category": "weapons",
    "name": "Bows",
    "rank": 2,
    "progress": 68,
    "required": 100,
    "description": "Practice with aimed attacks.",
    "next": "Next bow technique becomes available for review.",
    "techniques": [
      {
        "name": "Steady aim",
        "known": true
      },
      {
        "name": "Piercing shot",
        "known": false
      }
    ],
    "history": [
      "Last encounter: bow practice recorded."
    ]
  },
  {
    "id": "restoration",
    "category": "skills",
    "name": "Restoration",
    "rank": 2,
    "progress": 35,
    "required": 100,
    "description": "Practice with restorative skills.",
    "next": "Next restoration technique becomes available for review.",
    "techniques": [
      {
        "name": "Mend",
        "known": true
      },
      {
        "name": "Renew",
        "known": false
      }
    ],
    "history": [
      "Last encounter: restoration practice recorded."
    ]
  },
  {
    "id": "guarding",
    "category": "skills",
    "name": "Guarding",
    "rank": 1,
    "progress": 80,
    "required": 100,
    "description": "Practice with defensive skills.",
    "next": "Next defensive technique becomes available for review.",
    "techniques": [
      {
        "name": "Brace",
        "known": true
      },
      {
        "name": "Counterguard",
        "known": false
      }
    ],
    "history": [
      "Last encounter: defensive practice recorded."
    ]
  }
]
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "categories": [
    "weapons",
    "skills"
  ],
  "categoryIds": {
    "weapons": "weapons",
    "skills": "skills"
  },
  "defaultCategory": "weapons",
  "showLockedTechniques": true,
  "showHistory": true,
  "allowPreviewAward": true,
  "previewAward": 12,
  "progressMinimum": 0,
  "progressMaximum": 100,
  "labels": {
    "title": "Proficiencies",
    "back": "Back",
    "award": "Preview progress"
  },
  "layout": {
    "width": "min(95vw, 68rem)",
    "height": "min(90vh, 48rem)",
    "inset": "1rem",
    "gap": ".75rem",
    "sidebar": "minmax(0, 1fr)",
    "list": "minmax(0, 1fr)",
    "detail": "minmax(0, 2fr)",
    "meterHeight": ".6rem",
    "compactBreakpoint": "42rem",
    "standardButtonHeight": "2.75rem",
    "tileHeightMultiplier": 2,
    "categoryButtonSize": "full-standard",
    "tileButtonSize": "full-double"
  },
  "colors": {
    "background": "#241c13",
    "inset": "#19140e",
    "text": "#efe5d4",
    "muted": "#c1ad8b",
    "gold": "#d1aa60",
    "progress": "#4189b6",
    "primary": "#557b3f",
    "danger": "#ac4e3c"
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe W1x2: Skill proficiency

**Parent: W1x.** Use cases: Skills category; same workspace model. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
┌──────────────────────────────────────────────────────────────────────────┐
│ Proficiencies                                                        [×] │
├──────────────────┬──────────────────┬────────────────────────────────────┤
│ [    Weapons   ] │ ┌──────────────┐ │ Swords                      Rank 3 │
│ [    Skills    ] │ │ Swords     3 │ │ [████████░░░░░░░░░░░░░░]    42/100 │
│                  │ │ [██░] 42/100 │ │ Next benefit                       │
│                  │ └──────────────┘ │ Known techniques                   │
│                  │ ┌──────────────┐ │ Recent practice                    │
│                  │ │ Bows       2 │ │                                    │
│                  │ │ [██░] 68/100 │ │                                    │
│                  │ └──────────────┘ │                                    │
├──────────────────┴──────────────────┴────────────────────────────────────┤
│ [                                 Back                                 ] │
└──────────────────────────────────────────────────────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| W1x2.root | parent body width | parent body height | owning component slot | W1x active body | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
┌───────────────────────────┐
│ Proficiencies          [×] │
│ [Weapons ▾]  [Swords ▾]   │
├───────────────────────────┤
│ Swords            Rank 3  │
│ [████░░░░░░]      42/100  │
│ Next benefit              │
│ Known techniques          │
│ Recent practice           │
├───────────────────────────┤
│ [         Back          ] │
└───────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| W1x2.root | parent body width | parent body height | owning component slot | W1x active body | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Proficiencies          [×] │
│ [Weapons ▾]  [Swords ▾]   │
├───────────────────────────┤
│ Swords            Rank 3  │
│ [████░░░░░░]      42/100  │
│ Next benefit              │
│ Known techniques          │
│ Recent practice           │
├───────────────────────────┤
│ [         Back          ] │
└───────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| W1x2.root | parent body width | parent body height | owning component slot | W1x active body | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────┐
│ Proficiencies          [×] │
│ [Weapons ▾]  [Swords ▾]   │
├───────────────────────────┤
│ Swords            Rank 3  │
│ [████░░░░░░]      42/100  │
│ Next benefit              │
│ Known techniques          │
│ Recent practice           │
├───────────────────────────┤
│ [         Back          ] │
└───────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| W1x2.root | parent body width | parent body height | owning component slot | W1x active body | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT character knowledge, progression projection, config.progression
// Category and proficiency selection are presentation state, not progression mutations.
category = ResolveCategoryOrDefault(config.progression.categories, config.progression.defaultCategory)
entries = ProjectKnownProficiencies(character, category)
selected = ResolveStableSelection(entries)
ComposeW1Shell(title=config.progression.labels.title)
// Columns share a top anchor; category buttons and tiles never stretch to fill height.
// Shell inset alone defines outer left/right anchors; add gutters only at internal dividers.
// Compensate column tracks for unequal gutter counts so usable button widths match.
AlignDetailHeadingsMetersAndTextToOneInlineStart()
SetCategoryButtonHeight(config.progression.layout.standardButtonHeight)
SetTileHeight(config.progression.layout.standardButtonHeight * config.progression.layout.tileHeightMultiplier)
SetColumnTracks(config.progression.layout.sidebar, config.progression.layout.list, config.progression.layout.detail)
// Category and tile tracks have equal width and equal horizontal insets.
UseUniformWidthWithinEachColumn(); UseSharedColumnInset(config.progression.layout.inset)
AlignColumnsToTop(); SeparateColumnsWithVerticalRules()
Compose(WGP1, entries); Compose(WGP2, selected); Compose(WGP3, selected); Compose(WGP4, selected)
// Compact hosts replace navigation columns with dropdowns; retain the same selected ID.
AdaptNavigationToHost(config.progression.layout.compactBreakpoint)
RenderOnlyKnownBenefitsAndTechniques(character.knowledge)
If config.progression.showHistory: RenderOrderedPracticeEvents(selected)
// Back is the sole production footer command and therefore fills the footer.
Compose(WCB4, width=AvailableFooterWidth())
// Preview award is outside the game frame and changes only cloned fixture state.
If config.progression.allowPreviewAward: ExposePreviewAward(config.progression.previewAward)
// Each viewport gets isolated preview state; controls stay outside the game frame.
instance = ClonePresentationState(config.progression)
ExposeInstanceControls(instance.showLockedTechniques, instance.showHistory, instance.previewAward)
OnPreviewControlChange: ReprojectOnlyThisInstance(instance)
OnResetPreview: RestoreThisInstanceDefaults(config.progression)
SelectCategory(config.progression.categoryIds.skills) // Bind configured category ID, not a mechanics rule.
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

The existing equipment screen, equipment cards, modal shell and meters are source references for presentation reuse. Per-weapon and per-skill practice progression is proposed; these sample ranks, names and awards are illustrative, not shipped mechanics.

- [src/ui/screens/equipment.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/equipment.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/resbars.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/resbars.js#L1) — Current owner checkout (including inspected local edits)
- [PROGRESSION-SPECIFICATION.md](PROGRESSION-SPECIFICATION.md) — Proposed reference specification

**Referenced components:** [WGP1](wireframe-gallery.html#WGP1), [WGP2](wireframe-gallery.html#WGP2), [WGP3](wireframe-gallery.html#WGP3), [WGP4](wireframe-gallery.html#WGP4), [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [progression-reference-client.js](progression-reference-client.js), [progression-reference.mjs](progression-reference.mjs)

Renderer binding: `renderProgression`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [progression-reference.css](progression-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
[
  {
    "id": "swords",
    "category": "weapons",
    "name": "Swords",
    "rank": 3,
    "progress": 42,
    "required": 100,
    "description": "Practice with sword techniques.",
    "next": "Next sword technique becomes available for review.",
    "techniques": [
      {
        "name": "Measured cut",
        "known": true
      },
      {
        "name": "Guard break",
        "known": false
      }
    ],
    "history": [
      "Last encounter: sword practice recorded."
    ]
  },
  {
    "id": "bows",
    "category": "weapons",
    "name": "Bows",
    "rank": 2,
    "progress": 68,
    "required": 100,
    "description": "Practice with aimed attacks.",
    "next": "Next bow technique becomes available for review.",
    "techniques": [
      {
        "name": "Steady aim",
        "known": true
      },
      {
        "name": "Piercing shot",
        "known": false
      }
    ],
    "history": [
      "Last encounter: bow practice recorded."
    ]
  },
  {
    "id": "restoration",
    "category": "skills",
    "name": "Restoration",
    "rank": 2,
    "progress": 35,
    "required": 100,
    "description": "Practice with restorative skills.",
    "next": "Next restoration technique becomes available for review.",
    "techniques": [
      {
        "name": "Mend",
        "known": true
      },
      {
        "name": "Renew",
        "known": false
      }
    ],
    "history": [
      "Last encounter: restoration practice recorded."
    ]
  },
  {
    "id": "guarding",
    "category": "skills",
    "name": "Guarding",
    "rank": 1,
    "progress": 80,
    "required": 100,
    "description": "Practice with defensive skills.",
    "next": "Next defensive technique becomes available for review.",
    "techniques": [
      {
        "name": "Brace",
        "known": true
      },
      {
        "name": "Counterguard",
        "known": false
      }
    ],
    "history": [
      "Last encounter: defensive practice recorded."
    ]
  }
]
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "categories": [
    "weapons",
    "skills"
  ],
  "categoryIds": {
    "weapons": "weapons",
    "skills": "skills"
  },
  "defaultCategory": "weapons",
  "showLockedTechniques": true,
  "showHistory": true,
  "allowPreviewAward": true,
  "previewAward": 12,
  "progressMinimum": 0,
  "progressMaximum": 100,
  "labels": {
    "title": "Proficiencies",
    "back": "Back",
    "award": "Preview progress"
  },
  "layout": {
    "width": "min(95vw, 68rem)",
    "height": "min(90vh, 48rem)",
    "inset": "1rem",
    "gap": ".75rem",
    "sidebar": "minmax(0, 1fr)",
    "list": "minmax(0, 1fr)",
    "detail": "minmax(0, 2fr)",
    "meterHeight": ".6rem",
    "compactBreakpoint": "42rem",
    "standardButtonHeight": "2.75rem",
    "tileHeightMultiplier": 2,
    "categoryButtonSize": "full-standard",
    "tileButtonSize": "full-double"
  },
  "colors": {
    "background": "#241c13",
    "inset": "#19140e",
    "text": "#efe5d4",
    "muted": "#c1ad8b",
    "gold": "#d1aa60",
    "progress": "#4189b6",
    "primary": "#557b3f",
    "danger": "#ac4e3c"
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCB0: Buttons and inspection

**Parent: none.** Use cases: Buttons and inspection reusable component family. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Host content width = 100%
Presets: quarter 25% · third 30%
         half 50% · full 100%
Choices [ equal half ] gap [ equal half ]
Footer  [    Back    ] gap [   Confirm  ]
Solo    [            Full             ]
Header  Title                       [×]
Owner                (i)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Host content width = 100%
Presets: quarter 25% · third 30%
         half 50% · full 100%
Choices [ equal half ] gap [ equal half ]
Footer  [    Back    ] gap [   Confirm  ]
Solo    [            Full             ]
Header  Title                       [×]
Owner                (i)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Host content width = 100%
Presets: quarter 25% · third 30%
         half 50% · full 100%
Choices [ equal half ] gap [ equal half ]
Footer  [    Back    ] gap [   Confirm  ]
Solo    [            Full             ]
Header  Title                       [×]
Owner                (i)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Host content width = 100%
Presets: quarter 25% · third 30%
         half 50% · full 100%
Choices [ equal half ] gap [ equal half ]
Footer  [    Back    ] gap [   Confirm  ]
Solo    [            Full             ]
Header  Title                       [×]
Owner                (i)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Resolve every control through the shared action contract and config.buttonWidths.presets. Percentages reference the owning host content width; vw is valid only for a viewport-width host. Sibling text buttons use the same selected preset and equal gap-aware widths. Choice controls default to the half preset. Footer siblings divide the available width after gaps equally, remain inline, and a sole footer action uses full width. Exit, inspect, status-icon, map-node and packed combat-footer controls retain their explicitly configured compact or circular geometry. Inherit focus, disabled, busy, selected, and semantic role tokens.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)

// Width presets are configuration data; percentages use the owning content box.
hostWidth = MeasureContentWidth(context.buttonHost)
role = ResolveButtonRole(model, context)
preset = ResolveRolePreset(role, config.buttonWidths)
// Choice role resolves to the shared half preset by default.
requestedWidth = hostWidth * PercentFraction(config.buttonWidths.presets[preset])
siblings = FilterVisibleSiblingButtons(IncludingCurrentButton(context.actionGroup, model))
IF HasCompactGeometryException(context, config.buttonWidths)
  width = ResolveConfiguredCompactGeometry(context)
ELSE IF IsFooter(context) AND IsSoleAction(siblings)
  width = hostWidth
ELSE IF IsFooter(context) OR SharesButtonWidth(siblings)
  gapWidth = BetweenItemGapCount(siblings) * ResolveSharedGap(config)
  equalShare = AvailableAfterGaps(hostWidth, gapWidth) / Count(siblings)
  width = IF IsFooter(context) THEN equalShare ELSE FitPresetToShare(requestedWidth, equalShare)
ELSE
  width = FitToHost(requestedWidth, hostWidth)
// Apply the same resolved width to sibling text buttons, regardless of label length.
ApplyEqualSiblingWidths(siblings, width)
// Percent CSS is local to the host. Use viewport units only when the host is the viewport.
KeepFooterInline(); PreserveConfiguredReadableTextAndTargetMinimums()
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the buttons source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/kit/index.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/kit/index.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/confirmationModal.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/confirmationModal.js#L1) — Current owner checkout (including inspected local edits)
- [styles/kit.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/kit.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB1](wireframe-gallery.html#WCB1), [WCB2](wireframe-gallery.html#WCB2), [WCB3](wireframe-gallery.html#WCB3), [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCB0",
  "contextRef": "sample:buttons",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB1",
      "modelRef": "model:WCB1"
    },
    {
      "componentId": "WCB2",
      "modelRef": "model:WCB2"
    },
    {
      "componentId": "WCB3",
      "modelRef": "model:WCB3"
    },
    {
      "componentId": "WCB4",
      "modelRef": "model:WCB4"
    },
    {
      "componentId": "WCB5",
      "modelRef": "model:WCB5"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCB1: Inspect control

**Parent: WCB0.** Use cases: Every selectable card, combatant, inventory tile. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
              (i)
               │ center anchor
         selected owner
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB1.root | 2.75rem minimum target | 2.75rem minimum target | owning component slot | above owner top-center / above intent when present | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
              (i)
               │ center anchor
         selected owner
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB1.root | 2.75rem minimum target | 2.75rem minimum target | owning component slot | above owner top-center / above intent when present | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
              (i)
               │ center anchor
         selected owner
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB1.root | 2.75rem minimum target | 2.75rem minimum target | owning component slot | above owner top-center / above intent when present | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
              (i)
               │ center anchor
         selected owner
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB1.root | 2.75rem minimum target | 2.75rem minimum target | owning component slot | above owner top-center / above intent when present | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Schedule visibility with config.selection.revealDelayMs. Open knowledge-filtered W1w on activation; stop owner activation propagation and restore source focus on dismissal.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the inspect source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantInspector.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantInspector.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCB1",
  "contextRef": "sample:inspect",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCB2: Context action

**Parent: WCB0.** Use cases: Selected card host or modal footer. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
          selected card
[        Use · full host width        ]
             outside card footer
Choice group: [ half ] gap [ half ]
Other presets: 25% · 30% · 50% · 100%
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB2.root | usable host width | minimum target height | owning component slot | below selected card; outside its frame | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
          selected card
[        Use · full host width        ]
             outside card footer
Choice group: [ half ] gap [ half ]
Other presets: 25% · 30% · 50% · 100%
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB2.root | usable host width | minimum target height | owning component slot | below selected card; outside its frame | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
          selected card
[        Use · full host width        ]
             outside card footer
Choice group: [ half ] gap [ half ]
Other presets: 25% · 30% · 50% · 100%
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB2.root | usable host width | minimum target height | owning component slot | below selected card; outside its frame | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
          selected card
[        Use · full host width        ]
             outside card footer
Choice group: [ half ] gap [ half ]
Other presets: 25% · 30% · 50% · 100%
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB2.root | usable host width | minimum target height | owning component slot | below selected card; outside its frame | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Project domain action and readiness for applicable item and playing cards. Combatants do not own this host action: inspection uses delayed WCB1 and target selection uses the combatant itself. Omit absent actions. Resolve the named width preset through config.buttonWidths.presets; choice controls use config.buttonWidths.choice, default half. Sibling actions share an equal gap-aware width; sole footer action spans the entire footer content width. Await required target when applicable; revalidate once on commit. Never place Use inside metadata footer. Compact HUD and packed combat-footer contexts retain their declared geometry exceptions.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)

// Width presets are configuration data; percentages use the owning content box.
hostWidth = MeasureContentWidth(context.buttonHost)
role = ResolveButtonRole(model, context)
preset = ResolveRolePreset(role, config.buttonWidths)
// Choice role resolves to the shared half preset by default.
requestedWidth = hostWidth * PercentFraction(config.buttonWidths.presets[preset])
siblings = FilterVisibleSiblingButtons(IncludingCurrentButton(context.actionGroup, model))
IF HasCompactGeometryException(context, config.buttonWidths)
  width = ResolveConfiguredCompactGeometry(context)
ELSE IF IsFooter(context) AND IsSoleAction(siblings)
  width = hostWidth
ELSE IF IsFooter(context) OR SharesButtonWidth(siblings)
  gapWidth = BetweenItemGapCount(siblings) * ResolveSharedGap(config)
  equalShare = AvailableAfterGaps(hostWidth, gapWidth) / Count(siblings)
  width = IF IsFooter(context) THEN equalShare ELSE FitPresetToShare(requestedWidth, equalShare)
ELSE
  width = FitToHost(requestedWidth, hostWidth)
// Apply the same resolved width to sibling text buttons, regardless of label length.
ApplyEqualSiblingWidths(siblings, width)
// Percent CSS is local to the host. Use viewport units only when the host is the viewport.
KeepFooterInline(); PreserveConfiguredReadableTextAndTargetMinimums()
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the buttons source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/kit/index.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/kit/index.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/confirmationModal.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/confirmationModal.js#L1) — Current owner checkout (including inspected local edits)
- [styles/kit.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/kit.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCB2",
  "contextRef": "sample:buttons",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB3",
      "modelRef": "model:WCB3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCB3: Primary / confirmation button

**Parent: WCB0.** Use cases: Modal confirmation, contextual Use/Play/Equip. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Footer [ equal Back ] gap [ equal Confirm ]
Solo   [          Confirm · 100%          ]
Choice [ half 50% ] gap [ half 50% ]
Presets: 25% · 30% · 50% · 100% of host
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB3.root | shared action width | minimum target token | owning component slot | footer right; full-width when sole action | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Footer [ equal Back ] gap [ equal Confirm ]
Solo   [          Confirm · 100%          ]
Choice [ half 50% ] gap [ half 50% ]
Presets: 25% · 30% · 50% · 100% of host
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB3.root | shared action width | minimum target token | owning component slot | footer right; full-width when sole action | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Footer [ equal Back ] gap [ equal Confirm ]
Solo   [          Confirm · 100%          ]
Choice [ half 50% ] gap [ half 50% ]
Presets: 25% · 30% · 50% · 100% of host
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB3.root | shared action width | minimum target token | owning component slot | footer right; full-width when sole action | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Footer [ equal Back ] gap [ equal Confirm ]
Solo   [          Confirm · 100%          ]
Choice [ half 50% ] gap [ half 50% ]
Presets: 25% · 30% · 50% · 100% of host
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB3.root | shared action width | minimum target token | owning component slot | footer right; full-width when sole action | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Bind model.label and model.intent. Resolve widths from config.buttonWidths.presets and the configured role; percentages describe the owning content box, not the device viewport. Choice role defaults to half. Footer actions use equal shares of remaining inline width after configured gaps, or full when alone. Sibling labels never determine different button widths. Ready and focused primary uses configured green; disabled and busy states take precedence. Revalidate command at activation.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)

// Width presets are configuration data; percentages use the owning content box.
hostWidth = MeasureContentWidth(context.buttonHost)
role = ResolveButtonRole(model, context)
preset = ResolveRolePreset(role, config.buttonWidths)
// Choice role resolves to the shared half preset by default.
requestedWidth = hostWidth * PercentFraction(config.buttonWidths.presets[preset])
siblings = FilterVisibleSiblingButtons(IncludingCurrentButton(context.actionGroup, model))
IF HasCompactGeometryException(context, config.buttonWidths)
  width = ResolveConfiguredCompactGeometry(context)
ELSE IF IsFooter(context) AND IsSoleAction(siblings)
  width = hostWidth
ELSE IF IsFooter(context) OR SharesButtonWidth(siblings)
  gapWidth = BetweenItemGapCount(siblings) * ResolveSharedGap(config)
  equalShare = AvailableAfterGaps(hostWidth, gapWidth) / Count(siblings)
  width = IF IsFooter(context) THEN equalShare ELSE FitPresetToShare(requestedWidth, equalShare)
ELSE
  width = FitToHost(requestedWidth, hostWidth)
// Apply the same resolved width to sibling text buttons, regardless of label length.
ApplyEqualSiblingWidths(siblings, width)
// Percent CSS is local to the host. Use viewport units only when the host is the viewport.
KeepFooterInline(); PreserveConfiguredReadableTextAndTargetMinimums()
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the buttons source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/kit/index.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/kit/index.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/confirmationModal.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/confirmationModal.js#L1) — Current owner checkout (including inspected local edits)
- [styles/kit.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/kit.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCB3",
  "contextRef": "sample:buttons",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCB4: Back / cancel button

**Parent: WCB0.** Use cases: W0 modal footers and cancellation. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Footer [ equal Back ] gap [ equal Primary ]
Solo   [            Back · 100%           ]
Width follows the shared sibling preset
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB4.root | shared action width | minimum target token | owning component slot | footer left; full-width when sole action | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Footer [ equal Back ] gap [ equal Primary ]
Solo   [            Back · 100%           ]
Width follows the shared sibling preset
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB4.root | shared action width | minimum target token | owning component slot | footer left; full-width when sole action | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Footer [ equal Back ] gap [ equal Primary ]
Solo   [            Back · 100%           ]
Width follows the shared sibling preset
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB4.root | shared action width | minimum target token | owning component slot | footer left; full-width when sole action | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Footer [ equal Back ] gap [ equal Primary ]
Solo   [            Back · 100%           ]
Width follows the shared sibling preset
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB4.root | shared action width | minimum target token | owning component slot | footer left; full-width when sole action | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Apply shared action contract with dismissal role and the same config.buttonWidths preset as sibling footer actions. Deduct gaps before assigning equal widths; keep the footer inline, and span full host width when Back is the sole action. Use configured danger highlight on focus and pointer hover. Cancel presentation state and restore originating focus. Compact exit and icon controls inherit dismissal behavior while retaining their own target-size exception.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)

// Width presets are configuration data; percentages use the owning content box.
hostWidth = MeasureContentWidth(context.buttonHost)
role = ResolveButtonRole(model, context)
preset = ResolveRolePreset(role, config.buttonWidths)
// Choice role resolves to the shared half preset by default.
requestedWidth = hostWidth * PercentFraction(config.buttonWidths.presets[preset])
siblings = FilterVisibleSiblingButtons(IncludingCurrentButton(context.actionGroup, model))
IF HasCompactGeometryException(context, config.buttonWidths)
  width = ResolveConfiguredCompactGeometry(context)
ELSE IF IsFooter(context) AND IsSoleAction(siblings)
  width = hostWidth
ELSE IF IsFooter(context) OR SharesButtonWidth(siblings)
  gapWidth = BetweenItemGapCount(siblings) * ResolveSharedGap(config)
  equalShare = AvailableAfterGaps(hostWidth, gapWidth) / Count(siblings)
  width = IF IsFooter(context) THEN equalShare ELSE FitPresetToShare(requestedWidth, equalShare)
ELSE
  width = FitToHost(requestedWidth, hostWidth)
// Apply the same resolved width to sibling text buttons, regardless of label length.
ApplyEqualSiblingWidths(siblings, width)
// Percent CSS is local to the host. Use viewport units only when the host is the viewport.
KeepFooterInline(); PreserveConfiguredReadableTextAndTargetMinimums()
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the buttons source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/kit/index.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/kit/index.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/confirmationModal.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/confirmationModal.js#L1) — Current owner checkout (including inspected local edits)
- [styles/kit.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/kit.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCB4",
  "contextRef": "sample:buttons",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCB5: Exit button

**Parent: WCB0.** Use cases: Shared modal header. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
┌───┐
│ × │  width = height = config.buttonWidths.iconSize
└───┘  top-right shared inset
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB5.root | minimum target token | minimum target token | owning component slot | header top-right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
┌───┐
│ × │  width = height = config.buttonWidths.iconSize
└───┘  top-right shared inset
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB5.root | minimum target token | minimum target token | owning component slot | header top-right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───┐
│ × │  width = height = config.buttonWidths.iconSize
└───┘  top-right shared inset
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB5.root | minimum target token | minimum target token | owning component slot | header top-right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───┐
│ × │  width = height = config.buttonWidths.iconSize
└───┘  top-right shared inset
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCB5.root | minimum target token | minimum target token | owning component slot | header top-right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Use a square: set both inline and block size from config.buttonWidths.iconSize; prevent flex stretching and center glyph with zero internal padding. Use Close accessible label and shared dismissal policy. Respect pending modal decisions. Keep aligned to the header inset.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the buttons source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/kit/index.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/kit/index.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/confirmationModal.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/confirmationModal.js#L1) — Current owner checkout (including inspected local edits)
- [styles/kit.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/kit.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCB5",
  "contextRef": "sample:buttons",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCF0: Component foundations

**Parent: none.** Use cases: Component foundations reusable component family. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
MODEL → CONTRACT → VIEW
                ↘ Selection / active stack / inspector facts
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
MODEL → CONTRACT → VIEW
                ↘ Selection / active stack / inspector facts
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
MODEL → CONTRACT → VIEW
                ↘ Selection / active stack / inspector facts
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
MODEL → CONTRACT → VIEW
                ↘ Selection / active stack / inspector facts
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Project immutable values; validate registered component; compose contract, state projection, selection, and facts.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the contract source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/models/ComponentModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/ComponentModel.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/uiComponents.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/uiComponents.js#L1) — Current owner checkout (including inspected local edits)
- [styles/kit.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/kit.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCF1](wireframe-gallery.html#WCF1), [WCF2](wireframe-gallery.html#WCF2), [WCF3](wireframe-gallery.html#WCF3), [WCF4](wireframe-gallery.html#WCF4)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCF0",
  "contextRef": "sample:contract",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCF1",
      "modelRef": "model:WCF1"
    },
    {
      "componentId": "WCF2",
      "modelRef": "model:WCF2"
    },
    {
      "componentId": "WCF3",
      "modelRef": "model:WCF3"
    },
    {
      "componentId": "WCF4",
      "modelRef": "model:WCF4"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCF1: Component contract

**Parent: WCF0.** Use cases: All card and combatant parts. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Immutable model ──► validated component
Owner state ──────► presentation
Activation ───────► semantic intent
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF1.root | host allocation | host allocation | owning component slot | host slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Immutable model ──► validated component
Owner state ──────► presentation
Activation ───────► semantic intent
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF1.root | host allocation | host allocation | owning component slot | host slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Immutable model ──► validated component
Owner state ──────► presentation
Activation ───────► semantic intent
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF1.root | host allocation | host allocation | owning component slot | host slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Immutable model ──► validated component
Owner state ──────► presentation
Activation ───────► semantic intent
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF1.root | host allocation | host allocation | owning component slot | host slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Validate required identity and provider fields. Resolve inherited tokens. Render only active providers. Dispatch semantic intents without changing domain state.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the contract source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/models/ComponentModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/ComponentModel.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/uiComponents.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/uiComponents.js#L1) — Current owner checkout (including inspected local edits)
- [styles/kit.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/kit.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCF1",
  "contextRef": "sample:contract",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCF2: Ordered active stack

**Parent: WCF0.** Use cases: Combatant lower region. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
HP        [████████░░]
Resource  [██████░░░░]  if active
Buildup   [██████░░░░]  if budget admits
Stance    [Aggressive]  if active
Effects   [✚][☠][↓][+N] if present
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF2.root | 100% host width | content-fit, maximum5 rows | owning component slot | below sprite/name | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
HP        [████████░░]
Resource  [██████░░░░]  if active
Buildup   [██████░░░░]  if budget admits
Stance    [Aggressive]  if active
Effects   [✚][☠][↓][+N] if present
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF2.root | 100% host width | content-fit, maximum5 rows | owning component slot | below sprite/name | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
HP        [████████░░]
Resource  [██████░░░░]  if active
Buildup   [██████░░░░]  if budget admits
Stance    [Aggressive]  if active
Effects   [✚][☠][↓][+N] if present
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF2.root | 100% host width | content-fit, maximum5 rows | owning component slot | below sprite/name | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
HP        [████████░░]
Resource  [██████░░░░]  if active
Buildup   [██████░░░░]  if budget admits
Stance    [Aggressive]  if active
Effects   [✚][☠][↓][+N] if present
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF2.root | 100% host width | content-fit, maximum5 rows | owning component slot | below sprite/name | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Filter inactive providers before sorting by configured order. Reserve HP, present stance and icon row. Admit optional bars within config.meter.maxRows. Convert excess buildup to progress icons; collapse absent rows and gaps.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the status source boundary. The current working game renders status pips and collapsed proc progress in combat.js; the documentation dev baseline also has statusTray overflow disclosure. The five-row budget, icon-only tiles and inspector overflow route are owner-requested reference changes.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCM1](wireframe-gallery.html#WCM1), [WCM2](wireframe-gallery.html#WCM2), [WCM3](wireframe-gallery.html#WCM3), [WCM4](wireframe-gallery.html#WCM4), [WCM6](wireframe-gallery.html#WCM6)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCF2",
  "contextRef": "sample:status",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCM1",
      "modelRef": "model:WCM1"
    },
    {
      "componentId": "WCM2",
      "modelRef": "model:WCM2"
    },
    {
      "componentId": "WCM3",
      "modelRef": "model:WCM3"
    },
    {
      "componentId": "WCM4",
      "modelRef": "model:WCM4"
    },
    {
      "componentId": "WCM6",
      "modelRef": "model:WCM6"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCF3: Selection effect

**Parent: WCF0.** Use cases: All selectable cards and combatants. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
              (i) ← delayed reveal
  glow ╭────────────────────╮
       │ entire owner       │
       │ art + name + parts │
       ╰────────────────────╯
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF3.root | owner visual bounds | owner visual bounds | owning component slot | entire owner assembly including inspect | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
              (i) ← delayed reveal
  glow ╭────────────────────╮
       │ entire owner       │
       │ art + name + parts │
       ╰────────────────────╯
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF3.root | owner visual bounds | owner visual bounds | owning component slot | entire owner assembly including inspect | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
              (i) ← delayed reveal
  glow ╭────────────────────╮
       │ entire owner       │
       │ art + name + parts │
       ╰────────────────────╯
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF3.root | owner visual bounds | owner visual bounds | owning component slot | entire owner assembly including inspect | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
              (i) ← delayed reveal
  glow ╭────────────────────╮
       │ entire owner       │
       │ art + name + parts │
       ╰────────────────────╯
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF3.root | owner visual bounds | owner visual bounds | owning component slot | entire owner assembly including inspect | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Observe owner selection once. Apply inherited glow to the entire visible assembly, including the active lower stack. Reveal inspect after config.selection.revealDelayMs. Cancel pending reveal on deselection and disposal. Combatant selection exposes only the delayed inspect control; never create a separate Inspect or Eligible target action underneath. Clear stale item-card action UI when selecting a combatant.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the selection source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/hand.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hand.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB1](wireframe-gallery.html#WCB1), [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCF2](wireframe-gallery.html#WCF2)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCF3",
  "contextRef": "sample:selection",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB1",
      "modelRef": "model:WCB1"
    },
    {
      "componentId": "WCI1",
      "modelRef": "model:WCI1"
    },
    {
      "componentId": "WCI2",
      "modelRef": "model:WCI2"
    },
    {
      "componentId": "WCF2",
      "modelRef": "model:WCF2"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCF4: Inspector facts

**Parent: WCF0.** Use cases: W1w right pane. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
HP 32/40   Intent Attack 12   Defense 8
Current state   Mana 6/10 · Aggressive
Previous actions  Attack → Defend
Known abilities   Cleave
Known traits      Fire weakness
Lore              Knowledge-filtered prose
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF4.root | 100% detail pane | content-fit | owning component slot | top-left within detail section | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
HP 32/40   Intent Attack 12   Defense 8
Current state   Mana 6/10 · Aggressive
Previous actions  Attack → Defend
Known abilities   Cleave
Known traits      Fire weakness
Lore              Knowledge-filtered prose
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF4.root | 100% detail pane | content-fit | owning component slot | top-left within detail section | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
HP 32/40   Intent Attack 12   Defense 8
Current state   Mana 6/10 · Aggressive
Previous actions  Attack → Defend
Known abilities   Cleave
Known traits      Fire weakness
Lore              Knowledge-filtered prose
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF4.root | 100% detail pane | content-fit | owning component slot | top-left within detail section | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
HP 32/40   Intent Attack 12   Defense 8
Current state   Mana 6/10 · Aggressive
Previous actions  Attack → Defend
Known abilities   Cleave
Known traits      Fire weakness
Lore              Knowledge-filtered prose
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCF4.root | 100% detail pane | content-fit | owning component slot | top-left within detail section | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Project known values using registered detail providers. Render HP, intent and defense as facts, never embed sprite-overlay components in this pane. Render shared label/value columns in declared section order. Omit inactive current-state fields; unknown is distinct from absent. Preserve one independently scrolling details pane.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the inspector source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/combatantInspector.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantInspector.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/CombatantInspectorModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/CombatantInspectorModel.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCF4",
  "contextRef": "sample:inspector",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCI0: Identity and artwork

**Parent: none.** Use cases: Identity and artwork reusable component family. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Name
┌──────── Artwork ────────┐
│    intrinsic ratio     │
└────────────────────────┘
Rarity             Owned
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Name
┌──────── Artwork ────────┐
│    intrinsic ratio     │
└────────────────────────┘
Rarity             Owned
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Name
┌──────── Artwork ────────┐
│    intrinsic ratio     │
└────────────────────────┘
Rarity             Owned
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Name
┌──────── Artwork ────────┐
│    intrinsic ratio     │
└────────────────────────┘
Rarity             Owned
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Compose identity, contained artwork, and metadata only where the owner contract provides their slots. Do not copy the owner domain model into leaf state.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the identity source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WCI2](wireframe-gallery.html#WCI2), [WCI3](wireframe-gallery.html#WCI3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCI0",
  "contextRef": "sample:identity",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCI1",
      "modelRef": "model:WCI1"
    },
    {
      "componentId": "WCI2",
      "modelRef": "model:WCI2"
    },
    {
      "componentId": "WCI3",
      "modelRef": "model:WCI3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCI1: Nameplate

**Parent: WCI0.** Use cases: Card header, combatant above HP, inspector title. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
       Ashen Sentinel
       [ HP 32 / 40 ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI1.root | usable host width | content-fit | owning component slot | registered name slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
       Ashen Sentinel
       [ HP 32 / 40 ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI1.root | usable host width | content-fit | owning component slot | registered name slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
       Ashen Sentinel
       [ HP 32 / 40 ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI1.root | usable host width | content-fit | owning component slot | registered name slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
       Ashen Sentinel
       [ HP 32 / 40 ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI1.root | usable host width | content-fit | owning component slot | registered name slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Resolve localized display name from the identity provider; render text safely. Anchor to owner name slot immediately above HP for combatants.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the identity source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCI1",
  "contextRef": "sample:identity",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCI2: Sprite / artwork

**Parent: WCI0.** Use cases: Playing card art, combatant sprite, inspector preview. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
     ╭─ sprite bounds ─╮
     │      ◯          │
     │     ╱│╲         │
     │     ╱ ╲         │
     ╰──── baseline ───╯
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI2.root | contained host width | remaining host height | owning component slot | center; combatant baseline bottom | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
     ╭─ sprite bounds ─╮
     │      ◯          │
     │     ╱│╲         │
     │     ╱ ╲         │
     ╰──── baseline ───╯
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI2.root | contained host width | remaining host height | owning component slot | center; combatant baseline bottom | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
     ╭─ sprite bounds ─╮
     │      ◯          │
     │     ╱│╲         │
     │     ╱ ╲         │
     ╰──── baseline ───╯
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI2.root | contained host width | remaining host height | owning component slot | center; combatant baseline bottom | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
     ╭─ sprite bounds ─╮
     │      ◯          │
     │     ╱│╲         │
     │     ╱ ╲         │
     ╰──── baseline ───╯
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI2.root | contained host width | remaining host height | owning component slot | center; combatant baseline bottom | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Resolve asset and pose from the registry. Preserve intrinsic aspect ratio in allocated bounds. Mirror artwork for facing; do not mirror text or controls.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the identity source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCI2",
  "contextRef": "sample:identity",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCI3: Metadata footer

**Parent: WCI0.** Use cases: Item and playing-card footer. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Common                     Owned: 1
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI3.root | 100% usable card width | 10% card height | owning component slot | card bottom band | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Common                     Owned: 1
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI3.root | 100% usable card width | 10% card height | owning component slot | card bottom band | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Common                     Owned: 1
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI3.root | 100% usable card width | 10% card height | owning component slot | card bottom band | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Common                     Owned: 1
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCI3.root | 100% usable card width | 10% card height | owning component slot | card bottom band | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Project applicable metadata. Place rarity at start and ownership at end of the card metadata band. No domain action belongs in this band.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the identity source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCI3",
  "contextRef": "sample:identity",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCM0: Meters and statuses

**Parent: none.** Use cases: Meters and statuses reusable component family. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
HP      [████████░░] 32/40
Mana    [██████░░░░]  6/10
Buildup [██████░░░░] 65/100
Stance  [  Aggressive  ]
Icons   [✚][☠][↓][+N]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
HP      [████████░░] 32/40
Mana    [██████░░░░]  6/10
Buildup [██████░░░░] 65/100
Stance  [  Aggressive  ]
Icons   [✚][☠][↓][+N]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
HP      [████████░░] 32/40
Mana    [██████░░░░]  6/10
Buildup [██████░░░░] 65/100
Stance  [  Aggressive  ]
Icons   [✚][☠][↓][+N]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
HP      [████████░░] 32/40
Mana    [██████░░░░]  6/10
Buildup [██████░░░░] 65/100
Stance  [  Aggressive  ]
Icons   [✚][☠][↓][+N]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Compose the same meter primitive for health, resource and buildup with semantic model metadata. Stance and icons use shared tokens and active filtering.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the meter source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/resbars.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/resbars.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/resources.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/resources.js#L1) — Current owner checkout (including inspected local edits)
- [styles/kit.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/kit.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCM1](wireframe-gallery.html#WCM1), [WCM2](wireframe-gallery.html#WCM2), [WCM3](wireframe-gallery.html#WCM3), [WCM4](wireframe-gallery.html#WCM4), [WCM5](wireframe-gallery.html#WCM5), [WCM6](wireframe-gallery.html#WCM6)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCM0",
  "contextRef": "sample:meter",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCM1",
      "modelRef": "model:WCM1"
    },
    {
      "componentId": "WCM2",
      "modelRef": "model:WCM2"
    },
    {
      "componentId": "WCM3",
      "modelRef": "model:WCM3"
    },
    {
      "componentId": "WCM4",
      "modelRef": "model:WCM4"
    },
    {
      "componentId": "WCM5",
      "modelRef": "model:WCM5"
    },
    {
      "componentId": "WCM6",
      "modelRef": "model:WCM6"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCM1: Health meter

**Parent: WCM0.** Use cases: Combatants and inspector preview. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[HP ████████░░ 32 / 40]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM1.root | 100% stack width | HP height token (demo1.4rem) | owning component slot | below sprite/name | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[HP ████████░░ 32 / 40]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM1.root | 100% stack width | HP height token (demo1.4rem) | owning component slot | below sprite/name | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[HP ████████░░ 32 / 40]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM1.root | 100% stack width | HP height token (demo1.4rem) | owning component slot | below sprite/name | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[HP ████████░░ 32 / 40]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM1.root | 100% stack width | HP height token (demo1.4rem) | owning component slot | below sprite/name | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Read model.current and model.maximum. Clamp visual fill to valid display range, preserve original domain values. Provide accessible current and maximum labels.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the meter source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/resbars.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/resbars.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/resources.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/resources.js#L1) — Current owner checkout (including inspected local edits)
- [styles/kit.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/kit.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCM1",
  "contextRef": "sample:meter",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCM2: Resource meter

**Parent: WCM0.** Use cases: Active combatant resource rows, inspector facts. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[Mana ██████░░░░ 6 / 10]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM2.root | 100% stack width | 0.5 × HP height | owning component slot | after HP in ordered active stack | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[Mana ██████░░░░ 6 / 10]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM2.root | 100% stack width | 0.5 × HP height | owning component slot | after HP in ordered active stack | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Mana ██████░░░░ 6 / 10]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM2.root | 100% stack width | 0.5 × HP height | owning component slot | after HP in ordered active stack | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Mana ██████░░░░ 6 / 10]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM2.root | 100% stack width | 0.5 × HP height | owning component slot | after HP in ordered active stack | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Render only active resource providers in configured priority. Set height to config.meter.secondaryHeightRatio times HP height. Reuse meter semantics and color registry.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the meter source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/resbars.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/resbars.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/resources.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/resources.js#L1) — Current owner checkout (including inspected local edits)
- [styles/kit.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/kit.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCM1](wireframe-gallery.html#WCM1)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCM2",
  "contextRef": "sample:meter",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCM1",
      "modelRef": "model:WCM1"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCM3: Buildup meter

**Parent: WCM0.** Use cases: Threshold progress, overflow status icon. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[Burn buildup ██████░░░░ 65 / 100]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM3.root | 100% stack width | 0.5 × HP height | owning component slot | after resources | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[Burn buildup ██████░░░░ 65 / 100]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM3.root | 100% stack width | 0.5 × HP height | owning component slot | after resources | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Burn buildup ██████░░░░ 65 / 100]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM3.root | 100% stack width | 0.5 × HP height | owning component slot | after resources | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Burn buildup ██████░░░░ 65 / 100]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM3.root | 100% stack width | 0.5 × HP height | owning component slot | after resources | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Use threshold progress model distinct from status duration. Render admitted bars at configured secondary height; otherwise project a progress icon with stable identity.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the meter source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/resbars.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/resbars.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/resources.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/resources.js#L1) — Current owner checkout (including inspected local edits)
- [styles/kit.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/kit.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCM2](wireframe-gallery.html#WCM2)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCM3",
  "contextRef": "sample:meter",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCM2",
      "modelRef": "model:WCM2"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCM4: Stance strip

**Parent: WCM0.** Use cases: Active combatant stance and inspector fact. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[          Aggressive          ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM4.root | 100% stack width | 1 × HP height | owning component slot | after buildup / before icons | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[          Aggressive          ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM4.root | 100% stack width | 1 × HP height | owning component slot | after buildup / before icons | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[          Aggressive          ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM4.root | 100% stack width | 1 × HP height | owning component slot | after buildup / before icons | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[          Aggressive          ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM4.root | 100% stack width | 1 × HP height | owning component slot | after buildup / before icons | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Show active stance only. Set width equal to HP and height from config.meter.stanceHeightRatio. The informational strip does not issue a stance change.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the status source boundary. The current working game renders status pips and collapsed proc progress in combat.js; the documentation dev baseline also has statusTray overflow disclosure. The five-row budget, icon-only tiles and inspector overflow route are owner-requested reference changes.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCM4",
  "contextRef": "sample:status",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCM5: Status icon

**Parent: WCM0.** Use cases: Active effects and overflow buildup. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[☠]  ← icon only; details in tooltip
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM5.root | 1.575rem | 1.575rem | owning component slot | ordered bottom icon row | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[☠]  ← icon only; details in tooltip
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM5.root | 1.575rem | 1.575rem | owning component slot | ordered bottom icon row | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[☠]  ← icon only; details in tooltip
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM5.root | 1.575rem | 1.575rem | owning component slot | ordered bottom icon row | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[☠]  ← icon only; details in tooltip
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM5.root | 1.575rem | 1.575rem | owning component slot | ordered bottom icon row | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Use configured square icon size, icon asset and accessible name. Schedule shared tooltip for stacks, duration and effect description. Do not print extra counters inside icon boxes.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the status source boundary. The current working game renders status pips and collapsed proc progress in combat.js; the documentation dev baseline also has statusTray overflow disclosure. The five-row budget, icon-only tiles and inspector overflow route are owner-requested reference changes.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCM5",
  "contextRef": "sample:status",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCM6: Icon overflow

**Parent: WCM0.** Use cases: Crowded status rows. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[✚][☠][↓][+N] ← final reserved tile
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM6.root | same as WCM5 | same as WCM5 | owning component slot | final visible icon slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[✚][☠][↓][+N] ← final reserved tile
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM6.root | same as WCM5 | same as WCM5 | owning component slot | final visible icon slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[✚][☠][↓][+N] ← final reserved tile
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM6.root | same as WCM5 | same as WCM5 | owning component slot | final visible icon slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[✚][☠][↓][+N] ← final reserved tile
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCM6.root | same as WCM5 | same as WCM5 | owning component slot | final visible icon slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Measure available inline width and configured icon size and gap. If overflow exists, reserve final tile; N equals hidden count. Open complete inspector status section; never wrap into another row.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the status source boundary. The current working game renders status pips and collapsed proc progress in combat.js; the documentation dev baseline also has statusTray overflow disclosure. The five-row budget, icon-only tiles and inspector overflow route are owner-requested reference changes.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCM5](wireframe-gallery.html#WCM5)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCM6",
  "contextRef": "sample:status",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCM5",
      "modelRef": "model:WCM5"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCO0: Combat overlays

**Parent: none.** Use cases: Combat overlays reusable component family. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
         Intent
      ┌─ aura ───┐
      │ sprite  │  Defense ← role anchor
      │ + buff  │
      └─────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
         Intent
      ┌─ aura ───┐
      │ sprite  │  Defense ← role anchor
      │ + buff  │
      └─────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
         Intent
      ┌─ aura ───┐
      │ sprite  │  Defense ← role anchor
      │ + buff  │
      └─────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
         Intent
      ┌─ aura ───┐
      │ sprite  │  Defense ← role anchor
      │ + buff  │
      └─────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Compose active layers around a shared sprite rectangle. Keep effect layers noninteractive and controls above effects. Resolve role-facing placement from configuration.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the overlay source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCO1](wireframe-gallery.html#WCO1), [WCO2](wireframe-gallery.html#WCO2), [WCO3](wireframe-gallery.html#WCO3), [WCO4](wireframe-gallery.html#WCO4), [WCO5](wireframe-gallery.html#WCO5)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCO0",
  "contextRef": "sample:overlay",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCO1",
      "modelRef": "model:WCO1"
    },
    {
      "componentId": "WCO2",
      "modelRef": "model:WCO2"
    },
    {
      "componentId": "WCO3",
      "modelRef": "model:WCO3"
    },
    {
      "componentId": "WCO4",
      "modelRef": "model:WCO4"
    },
    {
      "componentId": "WCO5",
      "modelRef": "model:WCO5"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCO1: Intent indicator

**Parent: WCO0.** Use cases: Active announced combatant intent. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
       (i)
   [⚔ Attack · 12]
      sprite
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO1.root | content-fit | 2.8rem minimum | owning component slot | above sprite; below Inspect | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
       (i)
   [⚔ Attack · 12]
      sprite
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO1.root | content-fit | 2.8rem minimum | owning component slot | above sprite; below Inspect | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
       (i)
   [⚔ Attack · 12]
      sprite
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO1.root | content-fit | 2.8rem minimum | owning component slot | above sprite; below Inspect | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
       (i)
   [⚔ Attack · 12]
      sprite
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO1.root | content-fit | 2.8rem minimum | owning component slot | above sprite; below Inspect | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Require active announced intent and configured role visibility. Project preview supplied by domain; never reproduce AI calculations. Place below inspect above sprite.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the overlay source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCO1",
  "contextRef": "sample:overlay",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCO2: Defense badge

**Parent: WCO0.** Use cases: Active combatant block/defense. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
 sprite             [◇ 8]
         ← gap →    role anchor
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO2.root | 3.5rem minimum | 3.5rem minimum | owning component slot | sprite50% height; player right/enemy left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
 sprite             [◇ 8]
         ← gap →    role anchor
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO2.root | 3.5rem minimum | 3.5rem minimum | owning component slot | sprite50% height; player right/enemy left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
 sprite             [◇ 8]
         ← gap →    role anchor
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO2.root | 3.5rem minimum | 3.5rem minimum | owning component slot | sprite50% height; player right/enemy left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
 sprite             [◇ 8]
         ← gap →    role anchor
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO2.root | 3.5rem minimum | 3.5rem minimum | owning component slot | sprite50% height; player right/enemy left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Show active defense only. Anchor at config.overlay.defenseAnchorByRole[role] of sprite height with config.overlay.defenseGapRem external gap. Player upper-right (config.referenceTokens.value_12Percent.value), enemy lower-left (config.referenceTokens.value_88Percent.value).
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the overlay source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCO2",
  "contextRef": "sample:overlay",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCO3: Aura layer

**Parent: WCO0.** Use cases: Tag-driven combatant visual effects. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
┌── full sprite bounds ──┐
│ aura behind artwork   │
│                       │
└───────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO3.root | 100% sprite width | 100% sprite height | owning component slot | sprite bounds, behind artwork | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
┌── full sprite bounds ──┐
│ aura behind artwork   │
│                       │
└───────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO3.root | 100% sprite width | 100% sprite height | owning component slot | sprite bounds, behind artwork | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌── full sprite bounds ──┐
│ aura behind artwork   │
│                       │
└───────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO3.root | 100% sprite width | 100% sprite height | owning component slot | sprite bounds, behind artwork | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌── full sprite bounds ──┐
│ aura behind artwork   │
│                       │
└───────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO3.root | 100% sprite width | 100% sprite height | owning component slot | sprite bounds, behind artwork | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Resolve active effect provider. Fill complete sprite bounds behind artwork. Omit absent effect; never intercept pointer input or create domain buffs.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the overlay source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI2](wireframe-gallery.html#WCI2)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCO3",
  "contextRef": "sample:overlay",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCI2",
      "modelRef": "model:WCI2"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCO4: Buff layer

**Parent: WCO0.** Use cases: Active buff visual layer. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
┌── full sprite bounds ──┐
│ buff above artwork    │
│                       │
└───────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO4.root | sprite width minus shared inset | 100% sprite height | owning component slot | sprite bounds, above artwork | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
┌── full sprite bounds ──┐
│ buff above artwork    │
│                       │
└───────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO4.root | sprite width minus shared inset | 100% sprite height | owning component slot | sprite bounds, above artwork | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌── full sprite bounds ──┐
│ buff above artwork    │
│                       │
└───────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO4.root | sprite width minus shared inset | 100% sprite height | owning component slot | sprite bounds, above artwork | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌── full sprite bounds ──┐
│ buff above artwork    │
│                       │
└───────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO4.root | sprite width minus shared inset | 100% sprite height | owning component slot | sprite bounds, above artwork | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Resolve active buff visual provider. Cover full sprite height above artwork beneath controls. Respect reduced motion and disposal lifecycle.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the overlay source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI2](wireframe-gallery.html#WCI2)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCO4",
  "contextRef": "sample:overlay",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCI2",
      "modelRef": "model:WCI2"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCO5: Ground shadow

**Parent: WCO0.** Use cases: Shared sprite foot pivot. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
    sprite feet
───────┼─────── ground contact
     (shadow)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO5.root | config.groundShadow.widthPercent of sprite width | config.groundShadow.heightPercent of sprite height | owning component slot | beneath artwork; center on ground contact | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
    sprite feet
───────┼─────── ground contact
     (shadow)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO5.root | config.groundShadow.widthPercent of sprite width | config.groundShadow.heightPercent of sprite height | owning component slot | beneath artwork; center on ground contact | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
    sprite feet
───────┼─────── ground contact
     (shadow)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO5.root | config.groundShadow.widthPercent of sprite width | config.groundShadow.heightPercent of sprite height | owning component slot | beneath artwork; center on ground contact | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
    sprite feet
───────┼─────── ground contact
     (shadow)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCO5.root | config.groundShadow.widthPercent of sprite width | config.groundShadow.heightPercent of sprite height | owning component slot | beneath artwork; center on ground contact | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Render a noninteractive ellipse beneath sprite artwork. Its center is the shared ground-contact anchor. Resolve config.groundShadow dimensions and opacity. Preserve this anchor when status rows change; hidden shadow retains its logical pivot.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the overlay source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/combatantFrame.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/combatantFrame.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI2](wireframe-gallery.html#WCI2)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCO5",
  "contextRef": "sample:overlay",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCI2",
      "modelRef": "model:WCI2"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCT0: Tooltip components

**Parent: none.** Use cases: Tooltip components reusable component family. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[ Trigger ] ── delay ──► ┌ Tooltip ┐
                         │ detail  │
                         └────▽────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCT0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[ Trigger ] ── delay ──► ┌ Tooltip ┐
                         │ detail  │
                         └────▽────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCT0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[ Trigger ] ── delay ──► ┌ Tooltip ┐
                         │ detail  │
                         └────▽────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCT0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[ Trigger ] ── delay ──► ┌ Tooltip ┐
                         │ detail  │
                         └────▽────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCT0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Reuse one tooltip presenter with configured size variant and placement policy. Keep tooltips in current modal overlay layer.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the tooltip source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/tooltip.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/tooltip.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/TooltipPlacementModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/TooltipPlacementModel.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCT1](wireframe-gallery.html#WCT1)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCT0",
  "contextRef": "sample:tooltip",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCT1",
      "modelRef": "model:WCT1"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WCT1: Tooltip presenter

**Parent: WCT0.** Use cases: Tags, status/intent/defense explanations. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
┌ Poison ────────────────────┐
│ Damage over time.          │
│ 3 stacks · 2 turns         │
└────────────▽───────────────┘
             [☠]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCT1.root | WT1/WT2/WT3 token | content-fit | owning component slot | above trigger; flip/shift within viewport | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
┌ Poison ────────────────────┐
│ Damage over time.          │
│ 3 stacks · 2 turns         │
└────────────▽───────────────┘
             [☠]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCT1.root | WT1/WT2/WT3 token | content-fit | owning component slot | above trigger; flip/shift within viewport | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌ Poison ────────────────────┐
│ Damage over time.          │
│ 3 stacks · 2 turns         │
└────────────▽───────────────┘
             [☠]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCT1.root | WT1/WT2/WT3 token | content-fit | owning component slot | above trigger; flip/shift within viewport | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌ Poison ────────────────────┐
│ Damage over time.          │
│ 3 stacks · 2 turns         │
└────────────▽───────────────┘
             [☠]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WCT1.root | WT1/WT2/WT3 token | content-fit | owning component slot | above trigger; flip/shift within viewport | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// On hover or focus schedule after config.selection.tooltipDelayMs. Cancel stale requests, flip and shift to viewport bounds, dismiss on Escape or outside activation.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the tooltip source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/tooltip.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/tooltip.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/TooltipPlacementModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/TooltipPlacementModel.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WCT1",
  "contextRef": "sample:tooltip",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGC0: Combat composition

**Parent: none.** Use cases: Combat composition reusable component family. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Shared HUD + optional blue XP strip
Battlefield: player →   ← enemies
Hand: [card] [card] [card]
(A)[Draw][End turn][Discard](Potions)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Shared HUD + optional blue XP strip
Battlefield: player →   ← enemies
Hand: [card] [card] [card]
(A)[Draw][End turn][Discard](Potions)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Shared HUD + optional blue XP strip
Battlefield: player →   ← enemies
Hand: [card] [card] [card]
(A)[Draw][End turn][Discard](Potions)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Shared HUD + optional blue XP strip
Battlefield: player →   ← enemies
Hand: [card] [card] [card]
(A)[Draw][End turn][Discard](Potions)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Compose shared HUD, stage, hand and footer using scene band config. Child modules receive the same immutable snapshot and emit domain intents through the host dispatcher. WGC11 is the single Potions control; it opens WGH8 charge-flask and carried-potion contents inside the footer.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the stage source boundary. Current stage derives safe formation geometry from host and actor models. Reference section percentages and layer toggles are proposed, not replacements for production geometry validation.

- [src/ui/components/battlefieldStage.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/battlefieldStage.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/BattlefieldStageModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/BattlefieldStageModel.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGH4](wireframe-gallery.html#WGH4), [WGC1](wireframe-gallery.html#WGC1), [WGC5](wireframe-gallery.html#WGC5), [WGC6](wireframe-gallery.html#WGC6)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGC0",
  "contextRef": "sample:stage",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WGH4",
      "modelRef": "model:WGH4"
    },
    {
      "componentId": "WGC1",
      "modelRef": "model:WGC1"
    },
    {
      "componentId": "WGC5",
      "modelRef": "model:WGC5"
    },
    {
      "componentId": "WGC6",
      "modelRef": "model:WGC6"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGC1: Battlefield stage

**Parent: WCF2.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
┌ Skyline / floor ──────────────┐
│                              │
│ Player →      ← Foe  ← Foe   │
└──────── shared baseline ─────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC1.root | 100% | config.scene | owning component slot | below HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
┌ Skyline / floor ──────────────┐
│                              │
│ Player →      ← Foe  ← Foe   │
└──────── shared baseline ─────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC1.root | 100% | config.scene | owning component slot | below HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌ Skyline / floor ──────────────┐
│                              │
│ Player →      ← Foe  ← Foe   │
└──────── shared baseline ─────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC1.root | 100% | config.scene | owning component slot | below HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌ Skyline / floor ──────────────┐
│                              │
│ Player →      ← Foe  ← Foe   │
└──────── shared baseline ─────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC1.root | 100% | config.scene | owning component slot | below HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Own one selectedCombatantId for this battlefield. On selection replace the previous ID, clear its glow and hide its inspect control before highlighting the new actor. Project authored formation slots from host size and stable actor IDs. Compose player and enemy WC4 instances at config.scene.actorBaselinePercent of battlefield height. Align sprite feet to that baseline; labels and status stacks continue below. Paint skyline first, ground cutout second and actors above both. Preserve target overlays. Selection does not resize other actors: shared faction/category fit uses unselected cached envelopes, then adds selected growth at the stable foot anchor and highest focus layer.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the stage source boundary. Current stage derives safe formation geometry from host and actor models. Reference section percentages and layer toggles are proposed, not replacements for production geometry validation.

- [src/ui/components/battlefieldStage.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/battlefieldStage.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/BattlefieldStageModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/BattlefieldStageModel.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGS1](wireframe-gallery.html#WGS1), [WGC2](wireframe-gallery.html#WGC2), [WGC3](wireframe-gallery.html#WGC3), [WGC4](wireframe-gallery.html#WGC4)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGC1",
  "contextRef": "sample:stage",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WGS1",
      "modelRef": "model:WGS1"
    },
    {
      "componentId": "WGC2",
      "modelRef": "model:WGC2"
    },
    {
      "componentId": "WGC3",
      "modelRef": "model:WGC3"
    },
    {
      "componentId": "WGC4",
      "modelRef": "model:WGC4"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGC2: Player placement

**Parent: WC4.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Player slot →
   sprite          defense
   name
   HP / active stack
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC2.root | slot width | stage height | owning component slot | stage left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Player slot →
   sprite          defense
   name
   HP / active stack
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC2.root | slot width | stage height | owning component slot | stage left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Player slot →
   sprite          defense
   name
   HP / active stack
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC2.root | slot width | stage height | owning component slot | stage left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Player slot →
   sprite          defense
   name
   HP / active stack
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC2.root | slot width | stage height | owning component slot | stage left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Allocate player slots from config.scene.playerCount and stable actor IDs; render shared combatant cards with player-facing context. Intent is hidden by role default but configurable. Preserve sprite proportions and the same category base scale used by enemies. Apply configured row-base/selected-growth factors only after shared unselected fitting; anchor the sprite feet. Counts are preview fixture configuration, not a gameplay party-size rule.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the stage source boundary. Current stage derives safe formation geometry from host and actor models. Reference section percentages and layer toggles are proposed, not replacements for production geometry validation.

- [src/ui/components/battlefieldStage.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/battlefieldStage.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/BattlefieldStageModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/BattlefieldStageModel.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WC4a](wireframe-gallery.html#WC4a)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGC2",
  "contextRef": "sample:stage",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WC4a",
      "modelRef": "model:WC4a"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGC3: Enemy placements

**Parent: WC4.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
       ← Enemy       ← Enemy
       sprite        sprite
       name / HP     name / HP
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC3.root | slot widths | stage height | owning component slot | stage right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
       ← Enemy       ← Enemy
       sprite        sprite
       name / HP     name / HP
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC3.root | slot widths | stage height | owning component slot | stage right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
       ← Enemy       ← Enemy
       sprite        sprite
       name / HP     name / HP
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC3.root | slot widths | stage height | owning component slot | stage right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
       ← Enemy       ← Enemy
       sprite        sprite
       name / HP     name / HP
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC3.root | slot widths | stage height | owning component slot | stage right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Project stable enemy IDs to authored slots; the preview count comes from config.scene.enemyCount. Render the same combatant component with enemy-facing context and active intent; do not mirror controls or names. Use the same category base fit as allies, excluding selected details; preserve row depth and foot anchors under focus growth.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the stage source boundary. Current stage derives safe formation geometry from host and actor models. Reference section percentages and layer toggles are proposed, not replacements for production geometry validation.

- [src/ui/components/battlefieldStage.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/battlefieldStage.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/BattlefieldStageModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/BattlefieldStageModel.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WC4a](wireframe-gallery.html#WC4a)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGC3",
  "contextRef": "sample:stage",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WC4a",
      "modelRef": "model:WC4a"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGC4: Target layer

**Parent: WCF3.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Player       [eligible enemy]   unavailable
                    ↑ active target outline
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC4.root | 100% | 100% | owning component slot | over battlefield | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Player       [eligible enemy]   unavailable
                    ↑ active target outline
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC4.root | 100% | 100% | owning component slot | over battlefield | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Player       [eligible enemy]   unavailable
                    ↑ active target outline
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC4.root | 100% | 100% | owning component slot | over battlefield | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Player       [eligible enemy]   unavailable
                    ↑ active target outline
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC4.root | 100% | 100% | owning component slot | over battlefield | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Use domain-eligible entity IDs for hit regions and keyboard navigation. Highlight eligible and selected targets without selecting impossible targets. Clear stale selection on model change. Use the combatant hit region itself, never an additional Eligible target demo button.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the stage source boundary. Current stage derives safe formation geometry from host and actor models. Reference section percentages and layer toggles are proposed, not replacements for production geometry validation.

- [src/ui/components/battlefieldStage.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/battlefieldStage.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/BattlefieldStageModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/BattlefieldStageModel.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCF3](wireframe-gallery.html#WCF3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGC4",
  "contextRef": "sample:stage",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCF3",
      "modelRef": "model:WCF3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGC5: Hand region

**Parent: WC1.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[Attack] [Skill] [Power]
    proportional cards, shared selection
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC5.root | 100% | config.context | owning component slot | below battlefield | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[Attack] [Skill] [Power]
    proportional cards, shared selection
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC5.root | 100% | config.context | owning component slot | below battlefield | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Attack] [Skill] [Power]
    proportional cards, shared selection
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC5.root | 100% | config.context | owning component slot | below battlefield | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Attack] [Skill] [Power]
    proportional cards, shared selection
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC5.root | 100% | config.context | owning component slot | below battlefield | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Project stable hand cards through the shared WC1 renderer; reference fixtures resolve config.hand.fixtureIds through the registry. Interpolate capacity between config.hand.minCapacity and maxCapacity over narrowWidthRem to wideWidthRem, then constrain by minExposedTargetPx. Every card has the same scale bounded by minCardWidthRem and maxCardWidthRem; no per-card fitting. Use one bounded uniform card scale for the entire hand and preserve aspect ratio inside its section; focus/selection raises a card above its neighbors. Keep the configured exposed touch area for each unselected card; if the bounded cards cannot fit, use one horizontal hand scroller rather than shrinking below minimum or clipping targets. Do not create inner-card or vertical scrollbars; summarize text and inspect full details. Stable hit lanes remain selectable above the visual fan. Drag a card past config.hand.reorderThresholdPx to reorder the same card instance and snap to its nearest slot; Alt+Arrow keys provide the same operation. The single delayed inspect overlay is positioned entirely above the selected card and may escape the clipped hand. Do not implement draw or damage logic here.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the hand source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/hand.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hand.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/card.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/card.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WC1](wireframe-gallery.html#WC1)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGC5",
  "contextRef": "sample:hand",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WC1",
      "modelRef": "model:WC1"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGC6: Packed action footer

**Parent: WCF2.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
(Actions)[Draw][ End turn ][Discard](Potions)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC6.root | content-fit | config.footer | owning component slot | bottom-center | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
(Actions)[Draw][ End turn ][Discard](Potions)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC6.root | content-fit | config.footer | owning component slot | bottom-center | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
(Actions)[Draw][ End turn ][Discard](Potions)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC6.root | content-fit | config.footer | owning component slot | bottom-center | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
(Actions)[Draw][ End turn ][Discard](Potions)
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC6.root | content-fit | config.footer | owning component slot | bottom-center | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Compose controls in configured order as one tightly packed centered group. The nominal footer band is config.bands.footer, with minimum physical height config.footerLayout.minimumHeightPx. The hand absorbs the extra height required by that touch floor. Compute widths from config.footerLayout after subtracting gaps. Actions/Potions share a square diameter based on footer height times config.footerLayout.heightFraction, capped by allocated width while preserving the configured touch target; End turn shares their height and may stretch to its configured width fraction. Draw/Discard share their configured maximum width. Never overlap the hand. Empty non-End-turn controls fade.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the footer source boundary. Current combat and pile/flask source provides command and inspection behavior. The tightly packed circular footer is the proposed presentation layout.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/piles.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/piles.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/flask.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/flask.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGC7](wireframe-gallery.html#WGC7), [WGC8](wireframe-gallery.html#WGC8), [WGC9](wireframe-gallery.html#WGC9), [WGC10](wireframe-gallery.html#WGC10), [WGC11](wireframe-gallery.html#WGC11)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGC6",
  "contextRef": "sample:footer",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WGC7",
      "modelRef": "model:WGC7"
    },
    {
      "componentId": "WGC8",
      "modelRef": "model:WGC8"
    },
    {
      "componentId": "WGC9",
      "modelRef": "model:WGC9"
    },
    {
      "componentId": "WGC10",
      "modelRef": "model:WGC10"
    },
    {
      "componentId": "WGC11",
      "modelRef": "model:WGC11"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGC7: Actions remaining

**Parent: WCM2.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
  ( 3 )
 Actions
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC7.root | large control | large control | owning component slot | footer first | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
  ( 3 )
 Actions
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC7.root | large control | large control | owning component slot | footer first | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
  ( 3 )
 Actions
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC7.root | large control | large control | owning component slot | footer first | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
  ( 3 )
 Actions
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC7.root | large control | large control | owning component slot | footer first | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Read remaining actions from snapshot. Use large circular control size. Empty state fades and is announced accessibly; display does not spend actions.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the footer source boundary. Current combat and pile/flask source provides command and inspection behavior. The tightly packed circular footer is the proposed presentation layout.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/piles.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/piles.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/flask.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/flask.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGC7",
  "contextRef": "sample:footer",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGC8: Draw pile button

**Parent: WCB3.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[Draw · 12] → shared pile workspace
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC8.root | small control | small control | owning component slot | footer second | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[Draw · 12] → shared pile workspace
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC8.root | small control | small control | owning component slot | footer second | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Draw · 12] → shared pile workspace
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC8.root | small control | small control | owning component slot | footer second | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Draw · 12] → shared pile workspace
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC8.root | small control | small control | owning component slot | footer second | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Open draw pile inspector on activation. Use current pile count; no invented draw-card command. Empty state follows configured faded styling.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the footer source boundary. Current combat and pile/flask source provides command and inspection behavior. The tightly packed circular footer is the proposed presentation layout.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/piles.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/piles.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/flask.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/flask.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGC8",
  "contextRef": "sample:footer",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB3",
      "modelRef": "model:WCB3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGC9: End turn button

**Parent: WCB3.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[ End turn ]  ← center / large
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC9.root | large control | large control | owning component slot | footer center | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[ End turn ]  ← center / large
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC9.root | large control | large control | owning component slot | footer center | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[ End turn ]  ← center / large
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC9.root | large control | large control | owning component slot | footer center | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[ End turn ]  ← center / large
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC9.root | large control | large control | owning component slot | footer center | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Project turn readiness. Highlight green when legal and remaining actions exhausted or selected. Never fade solely because actions are empty. Revalidate end-turn command on activation.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the footer source boundary. Current combat and pile/flask source provides command and inspection behavior. The tightly packed circular footer is the proposed presentation layout.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/piles.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/piles.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/flask.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/flask.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGC9",
  "contextRef": "sample:footer",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB3",
      "modelRef": "model:WCB3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGC10: Discard/exhaust button

**Parent: WCB3.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[Discard · 4 / Exhaust · 1]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC10.root | small control | small control | owning component slot | footer fourth | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[Discard · 4 / Exhaust · 1]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC10.root | small control | small control | owning component slot | footer fourth | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Discard · 4 / Exhaust · 1]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC10.root | small control | small control | owning component slot | footer fourth | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Discard · 4 / Exhaust · 1]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC10.root | small control | small control | owning component slot | footer fourth | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Open shared categorized pile workspace with selected category. Counts come from snapshot; preserve one control between End turn and Potions.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the footer source boundary. Current combat and pile/flask source provides command and inspection behavior. The tightly packed circular footer is the proposed presentation layout.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/piles.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/piles.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/flask.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/flask.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGC10",
  "contextRef": "sample:footer",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB3",
      "modelRef": "model:WCB3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGC11: Potion control

**Parent: WCB3.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
(Potions)
  [HP ×2]
  [MP ×1]
  [Smoke vial ×1]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC11.root | large control | large control | owning component slot | footer fifth | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
(Potions)
  [HP ×2]
  [MP ×1]
  [Smoke vial ×1]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC11.root | large control | large control | owning component slot | footer fifth | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
(Potions)
  [HP ×2]
  [MP ×1]
  [Smoke vial ×1]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC11.root | large control | large control | owning component slot | footer fifth | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
(Potions)
  [HP ×2]
  [MP ×1]
  [Smoke vial ×1]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGC11.root | large control | large control | owning component slot | footer fifth | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Open shared WGH8 contents inside the Potions control: charge flasks and carried consumables are entries in one projection, never sibling HUD buttons. Resolve selected item action and target through domain. Use the shared large circular footer control and fade empty state.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the footer source boundary. Current combat and pile/flask source provides command and inspection behavior. The tightly packed circular footer is the proposed presentation layout.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/piles.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/piles.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/flask.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/flask.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGH8](wireframe-gallery.html#WGH8), [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs), [combatant-legibility.js](combatant-legibility.js), [combatant-focus.js](combatant-focus.js), [combatant-focus.mjs](combatant-focus.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css), [combatant-legibility.css](combatant-legibility.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGC11",
  "contextRef": "sample:footer",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WGH8",
      "modelRef": "model:WGH8"
    },
    {
      "componentId": "WCB3",
      "modelRef": "model:WCB3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGH0: HUD composition contract

**Parent: WCF2.** Use cases: WGH4 concrete composition; WGS2 shared scene slot. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[WGH7 Run header]
[WGH1 Vitality] [WGH2 Armoury] [WGH3 Menu]
[WGH6 Relics rail]
[WGH5 Experience strip when configured]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH0.root | 100% host width | content-fit within configured scene band | owning component slot | scene top; shared parent bounds | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[WGH7 Run header]
[WGH1 Vitality] [WGH2 Armoury] [WGH3 Menu]
[WGH6 Relics rail]
[WGH5 Experience strip when configured]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH0.root | 100% host width | content-fit within configured scene band | owning component slot | scene top; shared parent bounds | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[WGH7 Run header]
[WGH1 Vitality] [WGH2 Armoury] [WGH3 Menu]
[WGH6 Relics rail]
[WGH5 Experience strip when configured]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH0.root | 100% host width | content-fit within configured scene band | owning component slot | scene top; shared parent bounds | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[WGH7 Run header]
[WGH1 Vitality] [WGH2 Armoury] [WGH3 Menu]
[WGH6 Relics rail]
[WGH5 Experience strip when configured]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH0.root | 100% host width | content-fit within configured scene band | owning component slot | scene top; shared parent bounds | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT snapshot, context, config, commandRegistry
// This is a reference projection. Never award XP or spend inventory in a view.
model = ProjectKnownHudFields(snapshot, config.sample)
visible = FilterConfiguredActiveLayers(model, config.layers)
// Collapsed layers leave no reserved row or gap.
IF HostComposition() MATCHES config.phone.gate        // the published state, never a second measurement
  // One band in every context: stacked meters, Cinders, Armoury, Menu.
  visible = FilterConfiguredActiveLayers(model, Merge(config.layers, config.phone.layerOverrides))
  ComposeHeader(visible.cinders)
  ComposeStackedMeters(visible.vitality); ComposeActions(visible.armoury, visible.menu)
ELSE
  ComposeHeader(visible.class, visible.cinders, visible.position)
  ComposePrimaryRow(visible.vitality, visible.armoury, visible.menu)
// One Potions control owns flask charges and carried consumables.
potionEntries = ProjectPotions(snapshot, visible.chargeFlasks, visible.potions)
ComposeDetachedRail(visible.relics)
// Top HUD never renders Potions in either preset. Footer owns WGC11.
PublishFooterPotionsModel(config.potions.componentId, potionEntries)
// HP, MP and Smoke vial are revealed inside the FOOTER Potions control only.
OnPotionsActivate: OpenSharedPotionContents(potionEntries)
OnPotionChoice: EmitRegisteredUseIntent(); DomainRevalidatesReadiness()
IF visible.experience AND context IN config.experience.contexts
  RenderExperienceStrip(model.experience, config.experience)
// XP animation consumes an authoritative before/after settlement event.
ON combatSettled(event): AnimateProjectedFill(event.before, event.after, config.experience.animationMs)
ON action(intent): commandRegistry.dispatch(intent)
ON configurationChanged: ReprojectAndRender(); RestoreFocusedControl()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/viewModels/RunHudViewModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/viewModels/RunHudViewModel.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/hudmeta.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hudmeta.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGH4](wireframe-gallery.html#WGH4)

**Actual reference code:** [hud-reference-client.js](hud-reference-client.js), [hud-reference.mjs](hud-reference.mjs)

Renderer binding: `hudPlayground`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [hud-reference-client.js#hudStyleSheet](hud-reference-client.js#hudStyleSheet), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "className": "Warden",
  "cinders": 120,
  "act": 1,
  "floor": 4,
  "health": {
    "value": 32,
    "maximum": 40
  },
  "mana": {
    "value": 6,
    "maximum": 10
  },
  "stamina": {
    "value": 8,
    "maximum": 10
  },
  "experience": {
    "value": 40,
    "maximum": 100
  },
  "relics": [
    "Ash seal",
    "Ember charm"
  ],
  "potions": [
    "Smoke vial ×1"
  ],
  "chargeFlasks": [
    "HP ×2",
    "MP ×1"
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "context": "combat",
  "enabled": true,
  "layoutPreset": "proposed",
  "layers": {
    "header": true,
    "class": true,
    "cinders": true,
    "position": true,
    "vitality": true,
    "health": true,
    "mana": true,
    "stamina": true,
    "armoury": true,
    "menu": true,
    "rail": true,
    "relics": true,
    "potions": true,
    "experience": true,
    "chargeFlasks": true
  },
  "vitality": {
    "referenceMaximum": {
      "health": 200,
      "mana": 20,
      "stamina": 20
    },
    "maximumWidthPercent": 100,
    "scaleByMaximum": true
  },
  "potions": {
    "placement": "footerOnly",
    "componentId": "WGC11",
    "contentsComponentId": "WGH8",
    "openIntent": "openPotions",
    "combineChargeFlasks": true,
    "combineCarriedPotions": true
  },
  "experience": {
    "contexts": [
      "combat"
    ],
    "color": "#398bd1",
    "heightRem": 0.35,
    "animationMs": 650,
    "awardPreview": 15
  },
  "phone": {
    "gate": "data-layout='narrow'",
    "maxWidthPx": 520,
    "meters": "stacked",
    "layerOverrides": {
      "class": false,
      "position": false,
      "route": false
    }
  },
  "diagram": {
    "columns": {
      "wide": 62,
      "compact": 48,
      "portraitSE": 40,
      "portraitS24": 42
    },
    "trackColumns": 20,
    "labelColumns": 8
  },
  "layout": {
    "gapRem": 0.35,
    "insetRem": 0.5,
    "meterHeightRem": 1.15,
    "actionHeightRem": 2.75,
    "radiusRem": 0.25
  },
  "colors": {
    "background": "#211a12",
    "gold": "#d5af68",
    "text": "#eee2ca",
    "health": "#668c46",
    "mana": "#478dbc",
    "stamina": "#bf9949"
  },
  "sample": {
    "className": "Warden",
    "cinders": 120,
    "act": 1,
    "floor": 4,
    "health": {
      "value": 32,
      "maximum": 40
    },
    "mana": {
      "value": 6,
      "maximum": 10
    },
    "stamina": {
      "value": 8,
      "maximum": 10
    },
    "experience": {
      "value": 40,
      "maximum": 100
    },
    "relics": [
      "Ash seal",
      "Ember charm"
    ],
    "potions": [
      "Smoke vial ×1"
    ],
    "chargeFlasks": [
      "HP ×2",
      "MP ×1"
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGH1: Vitality HUD

**Parent: WCF2.** Use cases: WGH4; WGS2; combat and map HUD. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
HP      [███░]                32 / 40
MP      [██████░░░░]          6 / 10
Stamina [████████░░]          8 / 10
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH1.root | remaining primary row width | active meters × config.layout.meterHeightRem | owning component slot | primary row left; align meter edges | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
HP      [███░]                32 / 40
MP      [██████░░░░]          6 / 10
Stamina [████████░░]          8 / 10
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH1.root | remaining primary row width | active meters × config.layout.meterHeightRem | owning component slot | primary row left; align meter edges | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
HP      [███░]                32 / 40
MP      [██████░░░░]          6 / 10
Stamina [████████░░]          8 / 10
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH1.root | remaining primary row width | active meters × config.layout.meterHeightRem | owning component slot | primary row left; align meter edges | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
HP      [███░]                32 / 40
MP      [██████░░░░]          6 / 10
Stamina [████████░░]          8 / 10
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH1.root | remaining primary row width | active meters × config.layout.meterHeightRem | owning component slot | primary row left; align meter edges | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT resource snapshot, config.vitality, availableWidth
// Current source resourceBarPlan separates track length from fill.
FOR each configured active resource
  reference = config.vitality.referenceMaximum[resource.id]
  meterLaneWidth = ReserveExternalLabelColumns(availableWidth, localizedLabels, currentMaxValues)
  allowedWidth = meterLaneWidth * ClampUnit(PercentFraction(config.vitality.maximumWidthPercent))
  trackWidth = IF config.vitality.scaleByMaximum THEN ClampToRange(resource.maximum / reference * allowedWidth, EmptyLength(), allowedWidth) ELSE allowedWidth
  fillWidth = SafeProgressFraction(resource.current, resource.maximum) * trackWidth
  RenderTrack(trackWidth); RenderFill(fillWidth)
  RenderExternalValue(resource.current, resource.maximum)
// Current/max label occupies a shared outside column, never squeezed inside a short track.
// Reference maximum caps presentation width only; never caps domain maximum or value.
// Defaults and source references currently agree; read config rather than retyping their maxima.
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/hudmeta.js:92](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hudmeta.js#L92) — Current owner checkout (including inspected local edits)
- [src/ui/components/resbars.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/resbars.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/VitalsPanelModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/VitalsPanelModel.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/resources.js:196](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/resources.js#L196) — Current owner checkout (including inspected local edits)
- [src/content/resources.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/content/resources.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCM1](wireframe-gallery.html#WCM1), [WCM2](wireframe-gallery.html#WCM2)

**Actual reference code:** [hud-reference-client.js](hud-reference-client.js), [hud-reference.mjs](hud-reference.mjs)

Renderer binding: `hudPlayground`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [hud-reference-client.js#hudStyleSheet](hud-reference-client.js#hudStyleSheet), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "className": "Warden",
  "cinders": 120,
  "act": 1,
  "floor": 4,
  "health": {
    "value": 32,
    "maximum": 40
  },
  "mana": {
    "value": 6,
    "maximum": 10
  },
  "stamina": {
    "value": 8,
    "maximum": 10
  },
  "experience": {
    "value": 40,
    "maximum": 100
  },
  "relics": [
    "Ash seal",
    "Ember charm"
  ],
  "potions": [
    "Smoke vial ×1"
  ],
  "chargeFlasks": [
    "HP ×2",
    "MP ×1"
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "context": "combat",
  "enabled": true,
  "layoutPreset": "proposed",
  "layers": {
    "header": true,
    "class": true,
    "cinders": true,
    "position": true,
    "vitality": true,
    "health": true,
    "mana": true,
    "stamina": true,
    "armoury": true,
    "menu": true,
    "rail": true,
    "relics": true,
    "potions": true,
    "experience": true,
    "chargeFlasks": true
  },
  "vitality": {
    "referenceMaximum": {
      "health": 200,
      "mana": 20,
      "stamina": 20
    },
    "maximumWidthPercent": 100,
    "scaleByMaximum": true
  },
  "potions": {
    "placement": "footerOnly",
    "componentId": "WGC11",
    "contentsComponentId": "WGH8",
    "openIntent": "openPotions",
    "combineChargeFlasks": true,
    "combineCarriedPotions": true
  },
  "experience": {
    "contexts": [
      "combat"
    ],
    "color": "#398bd1",
    "heightRem": 0.35,
    "animationMs": 650,
    "awardPreview": 15
  },
  "phone": {
    "gate": "data-layout='narrow'",
    "maxWidthPx": 520,
    "meters": "stacked",
    "layerOverrides": {
      "class": false,
      "position": false,
      "route": false
    }
  },
  "diagram": {
    "columns": {
      "wide": 62,
      "compact": 48,
      "portraitSE": 40,
      "portraitS24": 42
    },
    "trackColumns": 20,
    "labelColumns": 8
  },
  "layout": {
    "gapRem": 0.35,
    "insetRem": 0.5,
    "meterHeightRem": 1.15,
    "actionHeightRem": 2.75,
    "radiusRem": 0.25
  },
  "colors": {
    "background": "#211a12",
    "gold": "#d5af68",
    "text": "#eee2ca",
    "health": "#668c46",
    "mana": "#478dbc",
    "stamina": "#bf9949"
  },
  "sample": {
    "className": "Warden",
    "cinders": 120,
    "act": 1,
    "floor": 4,
    "health": {
      "value": 32,
      "maximum": 40
    },
    "mana": {
      "value": 6,
      "maximum": 10
    },
    "stamina": {
      "value": 8,
      "maximum": 10
    },
    "experience": {
      "value": 40,
      "maximum": 100
    },
    "relics": [
      "Ash seal",
      "Ember charm"
    ],
    "potions": [
      "Smoke vial ×1"
    ],
    "chargeFlasks": [
      "HP ×2",
      "MP ×1"
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGH2: Armoury control

**Parent: WCB2.** Use cases: WGH4 primary row; equipment workspace. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[ ⚔ Armoury ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH2.root | content-fit | config.layout.actionHeightRem | owning component slot | right of vitality; before Menu | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[ ⚔ Armoury ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH2.root | content-fit | config.layout.actionHeightRem | owning component slot | right of vitality; before Menu | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[ ⚔ Armoury ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH2.root | content-fit | config.layout.actionHeightRem | owning component slot | right of vitality; before Menu | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[ ⚔ Armoury ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH2.root | content-fit | config.layout.actionHeightRem | owning component slot | right of vitality; before Menu | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
// Reuse WCB2. Project armoury command readiness; dispatch openArmoury intent. No loadout mutation in this control.
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/hudmeta.js:109](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hudmeta.js#L109) — Current owner checkout (including inspected local edits)
- [src/ui/models/QuickAccessPanelModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/QuickAccessPanelModel.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB2](wireframe-gallery.html#WCB2), [W1e](wireframe-gallery.html#W1e)

**Actual reference code:** [hud-reference-client.js](hud-reference-client.js), [hud-reference.mjs](hud-reference.mjs)

Renderer binding: `hudPlayground`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [hud-reference-client.js#hudStyleSheet](hud-reference-client.js#hudStyleSheet), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "className": "Warden",
  "cinders": 120,
  "act": 1,
  "floor": 4,
  "health": {
    "value": 32,
    "maximum": 40
  },
  "mana": {
    "value": 6,
    "maximum": 10
  },
  "stamina": {
    "value": 8,
    "maximum": 10
  },
  "experience": {
    "value": 40,
    "maximum": 100
  },
  "relics": [
    "Ash seal",
    "Ember charm"
  ],
  "potions": [
    "Smoke vial ×1"
  ],
  "chargeFlasks": [
    "HP ×2",
    "MP ×1"
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "context": "combat",
  "enabled": true,
  "layoutPreset": "proposed",
  "layers": {
    "header": true,
    "class": true,
    "cinders": true,
    "position": true,
    "vitality": true,
    "health": true,
    "mana": true,
    "stamina": true,
    "armoury": true,
    "menu": true,
    "rail": true,
    "relics": true,
    "potions": true,
    "experience": true,
    "chargeFlasks": true
  },
  "vitality": {
    "referenceMaximum": {
      "health": 200,
      "mana": 20,
      "stamina": 20
    },
    "maximumWidthPercent": 100,
    "scaleByMaximum": true
  },
  "potions": {
    "placement": "footerOnly",
    "componentId": "WGC11",
    "contentsComponentId": "WGH8",
    "openIntent": "openPotions",
    "combineChargeFlasks": true,
    "combineCarriedPotions": true
  },
  "experience": {
    "contexts": [
      "combat"
    ],
    "color": "#398bd1",
    "heightRem": 0.35,
    "animationMs": 650,
    "awardPreview": 15
  },
  "phone": {
    "gate": "data-layout='narrow'",
    "maxWidthPx": 520,
    "meters": "stacked",
    "layerOverrides": {
      "class": false,
      "position": false,
      "route": false
    }
  },
  "diagram": {
    "columns": {
      "wide": 62,
      "compact": 48,
      "portraitSE": 40,
      "portraitS24": 42
    },
    "trackColumns": 20,
    "labelColumns": 8
  },
  "layout": {
    "gapRem": 0.35,
    "insetRem": 0.5,
    "meterHeightRem": 1.15,
    "actionHeightRem": 2.75,
    "radiusRem": 0.25
  },
  "colors": {
    "background": "#211a12",
    "gold": "#d5af68",
    "text": "#eee2ca",
    "health": "#668c46",
    "mana": "#478dbc",
    "stamina": "#bf9949"
  },
  "sample": {
    "className": "Warden",
    "cinders": 120,
    "act": 1,
    "floor": 4,
    "health": {
      "value": 32,
      "maximum": 40
    },
    "mana": {
      "value": 6,
      "maximum": 10
    },
    "stamina": {
      "value": 8,
      "maximum": 10
    },
    "experience": {
      "value": 40,
      "maximum": 100
    },
    "relics": [
      "Ash seal",
      "Ember charm"
    ],
    "potions": [
      "Smoke vial ×1"
    ],
    "chargeFlasks": [
      "HP ×2",
      "MP ×1"
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGH3: Menu control

**Parent: WCB2.** Use cases: WGH4 primary row; quick menu. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[ ☰ Menu ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH3.root | content-fit | config.layout.actionHeightRem | owning component slot | primary row far right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[ ☰ Menu ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH3.root | content-fit | config.layout.actionHeightRem | owning component slot | primary row far right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[ ☰ Menu ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH3.root | content-fit | config.layout.actionHeightRem | owning component slot | primary row far right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[ ☰ Menu ]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH3.root | content-fit | config.layout.actionHeightRem | owning component slot | primary row far right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
// Reuse WCB2. Dispatch openMenu; focus first available menu control and restore trigger on dismissal.
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/hudmeta.js:109](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hudmeta.js#L109) — Current owner checkout (including inspected local edits)
- [src/ui/models/QuickAccessPanelModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/QuickAccessPanelModel.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB2](wireframe-gallery.html#WCB2), [W1a](wireframe-gallery.html#W1a)

**Actual reference code:** [hud-reference-client.js](hud-reference-client.js), [hud-reference.mjs](hud-reference.mjs)

Renderer binding: `hudPlayground`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [hud-reference-client.js#hudStyleSheet](hud-reference-client.js#hudStyleSheet), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "className": "Warden",
  "cinders": 120,
  "act": 1,
  "floor": 4,
  "health": {
    "value": 32,
    "maximum": 40
  },
  "mana": {
    "value": 6,
    "maximum": 10
  },
  "stamina": {
    "value": 8,
    "maximum": 10
  },
  "experience": {
    "value": 40,
    "maximum": 100
  },
  "relics": [
    "Ash seal",
    "Ember charm"
  ],
  "potions": [
    "Smoke vial ×1"
  ],
  "chargeFlasks": [
    "HP ×2",
    "MP ×1"
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "context": "combat",
  "enabled": true,
  "layoutPreset": "proposed",
  "layers": {
    "header": true,
    "class": true,
    "cinders": true,
    "position": true,
    "vitality": true,
    "health": true,
    "mana": true,
    "stamina": true,
    "armoury": true,
    "menu": true,
    "rail": true,
    "relics": true,
    "potions": true,
    "experience": true,
    "chargeFlasks": true
  },
  "vitality": {
    "referenceMaximum": {
      "health": 200,
      "mana": 20,
      "stamina": 20
    },
    "maximumWidthPercent": 100,
    "scaleByMaximum": true
  },
  "potions": {
    "placement": "footerOnly",
    "componentId": "WGC11",
    "contentsComponentId": "WGH8",
    "openIntent": "openPotions",
    "combineChargeFlasks": true,
    "combineCarriedPotions": true
  },
  "experience": {
    "contexts": [
      "combat"
    ],
    "color": "#398bd1",
    "heightRem": 0.35,
    "animationMs": 650,
    "awardPreview": 15
  },
  "phone": {
    "gate": "data-layout='narrow'",
    "maxWidthPx": 520,
    "meters": "stacked",
    "layerOverrides": {
      "class": false,
      "position": false,
      "route": false
    }
  },
  "diagram": {
    "columns": {
      "wide": 62,
      "compact": 48,
      "portraitSE": 40,
      "portraitS24": 42
    },
    "trackColumns": 20,
    "labelColumns": 8
  },
  "layout": {
    "gapRem": 0.35,
    "insetRem": 0.5,
    "meterHeightRem": 1.15,
    "actionHeightRem": 2.75,
    "radiusRem": 0.25
  },
  "colors": {
    "background": "#211a12",
    "gold": "#d5af68",
    "text": "#eee2ca",
    "health": "#668c46",
    "mana": "#478dbc",
    "stamina": "#bf9949"
  },
  "sample": {
    "className": "Warden",
    "cinders": 120,
    "act": 1,
    "floor": 4,
    "health": {
      "value": 32,
      "maximum": 40
    },
    "mana": {
      "value": 6,
      "maximum": 10
    },
    "stamina": {
      "value": 8,
      "maximum": 10
    },
    "experience": {
      "value": 40,
      "maximum": 100
    },
    "relics": [
      "Ash seal",
      "Ember charm"
    ],
    "potions": [
      "Smoke vial ×1"
    ],
    "chargeFlasks": [
      "HP ×2",
      "MP ×1"
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGH4: Total HUD

**Parent: WCF2.** Use cases: W4a W4b; WGS2 aliases this composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
┌──────────────────────────────────────────────────────────────┐
│Warden · Cinders 120 · Act 1 Floor 4                          │
│HP      [███░]                32 / 40                         │
│MP      [██████░░░░]          6 / 10                          │
│Stamina [████████░░]          8 / 10                          │
│[Armoury] [Menu]                                              │
│[Ash seal] [Ember charm]                                      │
└──────────────────────────────────────────────────────────────┘
█████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH4.root | 100% host / 100vw in full-screen scene | content-fit within configured scene HUD band | owning component slot | top full-width; XP directly below entire HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
┌──────────────────────────────────────────────────────────────┐
│Warden · Cinders 120 · Act 1 Floor 4                          │
│HP      [███░]                32 / 40                         │
│MP      [██████░░░░]          6 / 10                          │
│Stamina [████████░░]          8 / 10                          │
│[Armoury] [Menu]                                              │
│[Ash seal] [Ember charm]                                      │
└──────────────────────────────────────────────────────────────┘
█████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH4.root | 100% host / 100vw in full-screen scene | content-fit within configured scene HUD band | owning component slot | top full-width; XP directly below entire HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌────────────────────────────────────────┐
│Cinders 120                             │
│HP      [███░]                32 / 40   │
│MP      [██████░░░░]          6 / 10    │
│Stamina [████████░░]          8 / 10    │
│[Armoury] [Menu]                        │
│[Ash seal] [Ember charm]                │
└────────────────────────────────────────┘
████████████████░░░░░░░░░░░░░░░░░░░░░░░░
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH4.root | 100% host / 100vw in full-screen scene | content-fit within configured scene HUD band | owning component slot | top full-width; XP directly below entire HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌────────────────────────────────────────┐
│Cinders 120                             │
│HP      [███░]                32 / 40   │
│MP      [██████░░░░]          6 / 10    │
│Stamina [████████░░]          8 / 10    │
│[Armoury] [Menu]                        │
│[Ash seal] [Ember charm]                │
└────────────────────────────────────────┘
████████████████░░░░░░░░░░░░░░░░░░░░░░░░
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH4.root | 100% host / 100vw in full-screen scene | content-fit within configured scene HUD band | owning component slot | top full-width; XP directly below entire HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT snapshot, context, config, commandRegistry
// This is a reference projection. Never award XP or spend inventory in a view.
model = ProjectKnownHudFields(snapshot, config.sample)
visible = FilterConfiguredActiveLayers(model, config.layers)
// Collapsed layers leave no reserved row or gap.
IF HostComposition() MATCHES config.phone.gate        // the published state, never a second measurement
  // One band in every context: stacked meters, Cinders, Armoury, Menu.
  visible = FilterConfiguredActiveLayers(model, Merge(config.layers, config.phone.layerOverrides))
  ComposeHeader(visible.cinders)
  ComposeStackedMeters(visible.vitality); ComposeActions(visible.armoury, visible.menu)
ELSE
  ComposeHeader(visible.class, visible.cinders, visible.position)
  ComposePrimaryRow(visible.vitality, visible.armoury, visible.menu)
// One Potions control owns flask charges and carried consumables.
potionEntries = ProjectPotions(snapshot, visible.chargeFlasks, visible.potions)
ComposeDetachedRail(visible.relics)
// Top HUD never renders Potions in either preset. Footer owns WGC11.
PublishFooterPotionsModel(config.potions.componentId, potionEntries)
// HP, MP and Smoke vial are revealed inside the FOOTER Potions control only.
OnPotionsActivate: OpenSharedPotionContents(potionEntries)
OnPotionChoice: EmitRegisteredUseIntent(); DomainRevalidatesReadiness()
IF visible.experience AND context IN config.experience.contexts
  RenderExperienceStrip(model.experience, config.experience)
// XP animation consumes an authoritative before/after settlement event.
ON combatSettled(event): AnimateProjectedFill(event.before, event.after, config.experience.animationMs)
ON action(intent): commandRegistry.dispatch(intent)
ON configurationChanged: ReprojectAndRender(); RestoreFocusedControl()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/hudmeta.js:142](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hudmeta.js#L142) — Current owner checkout (including inspected local edits)
- [src/ui/viewModels/RunHudViewModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/viewModels/RunHudViewModel.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/runHud.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/runHud.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGH7](wireframe-gallery.html#WGH7), [WGH1](wireframe-gallery.html#WGH1), [WGH2](wireframe-gallery.html#WGH2), [WGH3](wireframe-gallery.html#WGH3), [WGH6](wireframe-gallery.html#WGH6), [WGH5](wireframe-gallery.html#WGH5)

**Actual reference code:** [hud-reference-client.js](hud-reference-client.js), [hud-reference.mjs](hud-reference.mjs)

Renderer binding: `hudPlayground`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [hud-reference-client.js#hudStyleSheet](hud-reference-client.js#hudStyleSheet), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "className": "Warden",
  "cinders": 120,
  "act": 1,
  "floor": 4,
  "health": {
    "value": 32,
    "maximum": 40
  },
  "mana": {
    "value": 6,
    "maximum": 10
  },
  "stamina": {
    "value": 8,
    "maximum": 10
  },
  "experience": {
    "value": 40,
    "maximum": 100
  },
  "relics": [
    "Ash seal",
    "Ember charm"
  ],
  "potions": [
    "Smoke vial ×1"
  ],
  "chargeFlasks": [
    "HP ×2",
    "MP ×1"
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "context": "combat",
  "enabled": true,
  "layoutPreset": "proposed",
  "layers": {
    "header": true,
    "class": true,
    "cinders": true,
    "position": true,
    "vitality": true,
    "health": true,
    "mana": true,
    "stamina": true,
    "armoury": true,
    "menu": true,
    "rail": true,
    "relics": true,
    "potions": true,
    "experience": true,
    "chargeFlasks": true
  },
  "vitality": {
    "referenceMaximum": {
      "health": 200,
      "mana": 20,
      "stamina": 20
    },
    "maximumWidthPercent": 100,
    "scaleByMaximum": true
  },
  "potions": {
    "placement": "footerOnly",
    "componentId": "WGC11",
    "contentsComponentId": "WGH8",
    "openIntent": "openPotions",
    "combineChargeFlasks": true,
    "combineCarriedPotions": true
  },
  "experience": {
    "contexts": [
      "combat"
    ],
    "color": "#398bd1",
    "heightRem": 0.35,
    "animationMs": 650,
    "awardPreview": 15
  },
  "phone": {
    "gate": "data-layout='narrow'",
    "maxWidthPx": 520,
    "meters": "stacked",
    "layerOverrides": {
      "class": false,
      "position": false,
      "route": false
    }
  },
  "diagram": {
    "columns": {
      "wide": 62,
      "compact": 48,
      "portraitSE": 40,
      "portraitS24": 42
    },
    "trackColumns": 20,
    "labelColumns": 8
  },
  "layout": {
    "gapRem": 0.35,
    "insetRem": 0.5,
    "meterHeightRem": 1.15,
    "actionHeightRem": 2.75,
    "radiusRem": 0.25
  },
  "colors": {
    "background": "#211a12",
    "gold": "#d5af68",
    "text": "#eee2ca",
    "health": "#668c46",
    "mana": "#478dbc",
    "stamina": "#bf9949"
  },
  "sample": {
    "className": "Warden",
    "cinders": 120,
    "act": 1,
    "floor": 4,
    "health": {
      "value": 32,
      "maximum": 40
    },
    "mana": {
      "value": 6,
      "maximum": 10
    },
    "stamina": {
      "value": 8,
      "maximum": 10
    },
    "experience": {
      "value": 40,
      "maximum": 100
    },
    "relics": [
      "Ash seal",
      "Ember charm"
    ],
    "potions": [
      "Smoke vial ×1"
    ],
    "chargeFlasks": [
      "HP ×2",
      "MP ×1"
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGH5: Experience strip

**Parent: WCM2.** Use cases: WGH4 below detached rail; combat-only default. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
█████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
Blue strip · 0.35rem · full host width
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH5.root | 100% host; 100vw when host is viewport | config.experience.heightRem | owning component slot | below total HUD; left-to-right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
█████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
Blue strip · 0.35rem · full host width
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH5.root | 100% host; 100vw when host is viewport | config.experience.heightRem | owning component slot | below total HUD; left-to-right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
████████████████░░░░░░░░░░░░░░░░░░░░░░░░
Blue strip · 0.35rem · full host width
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH5.root | 100% host; 100vw when host is viewport | config.experience.heightRem | owning component slot | below total HUD; left-to-right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
████████████████░░░░░░░░░░░░░░░░░░░░░░░░
Blue strip · 0.35rem · full host width
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH5.root | 100% host; 100vw when host is viewport | config.experience.heightRem | owning component slot | below total HUD; left-to-right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
// Proposed owner extension; not existing game XP rules.
IF config.layers.experience AND context IN config.experience.contexts
  ratio = SafeNormalizedProgress(snapshot.experience)
  RenderMeter(ratio, config.experience.color, config.experience.heightRem)
ON authoritativeCombatSettlement(event)
  AnimateFill(event.previousProgress, event.currentProgress, config.experience.animationMs)
// Announce progress through accessible meter label; level thresholds come from domain.
// Ignore duplicate settlement IDs; reduced-motion uses immediate final projection.
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- PROPOSED: combat settlement experience projection; no production mechanic implemented

**Referenced components:** [WCM2](wireframe-gallery.html#WCM2)

**Actual reference code:** [hud-reference-client.js](hud-reference-client.js), [hud-reference.mjs](hud-reference.mjs)

Renderer binding: `hudPlayground`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [hud-reference-client.js#hudStyleSheet](hud-reference-client.js#hudStyleSheet), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "className": "Warden",
  "cinders": 120,
  "act": 1,
  "floor": 4,
  "health": {
    "value": 32,
    "maximum": 40
  },
  "mana": {
    "value": 6,
    "maximum": 10
  },
  "stamina": {
    "value": 8,
    "maximum": 10
  },
  "experience": {
    "value": 40,
    "maximum": 100
  },
  "relics": [
    "Ash seal",
    "Ember charm"
  ],
  "potions": [
    "Smoke vial ×1"
  ],
  "chargeFlasks": [
    "HP ×2",
    "MP ×1"
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "context": "combat",
  "enabled": true,
  "layoutPreset": "proposed",
  "layers": {
    "header": true,
    "class": true,
    "cinders": true,
    "position": true,
    "vitality": true,
    "health": true,
    "mana": true,
    "stamina": true,
    "armoury": true,
    "menu": true,
    "rail": true,
    "relics": true,
    "potions": true,
    "experience": true,
    "chargeFlasks": true
  },
  "vitality": {
    "referenceMaximum": {
      "health": 200,
      "mana": 20,
      "stamina": 20
    },
    "maximumWidthPercent": 100,
    "scaleByMaximum": true
  },
  "potions": {
    "placement": "footerOnly",
    "componentId": "WGC11",
    "contentsComponentId": "WGH8",
    "openIntent": "openPotions",
    "combineChargeFlasks": true,
    "combineCarriedPotions": true
  },
  "experience": {
    "contexts": [
      "combat"
    ],
    "color": "#398bd1",
    "heightRem": 0.35,
    "animationMs": 650,
    "awardPreview": 15
  },
  "phone": {
    "gate": "data-layout='narrow'",
    "maxWidthPx": 520,
    "meters": "stacked",
    "layerOverrides": {
      "class": false,
      "position": false,
      "route": false
    }
  },
  "diagram": {
    "columns": {
      "wide": 62,
      "compact": 48,
      "portraitSE": 40,
      "portraitS24": 42
    },
    "trackColumns": 20,
    "labelColumns": 8
  },
  "layout": {
    "gapRem": 0.35,
    "insetRem": 0.5,
    "meterHeightRem": 1.15,
    "actionHeightRem": 2.75,
    "radiusRem": 0.25
  },
  "colors": {
    "background": "#211a12",
    "gold": "#d5af68",
    "text": "#eee2ca",
    "health": "#668c46",
    "mana": "#478dbc",
    "stamina": "#bf9949"
  },
  "sample": {
    "className": "Warden",
    "cinders": 120,
    "act": 1,
    "floor": 4,
    "health": {
      "value": 32,
      "maximum": 40
    },
    "mana": {
      "value": 6,
      "maximum": 10
    },
    "stamina": {
      "value": 8,
      "maximum": 10
    },
    "experience": {
      "value": 40,
      "maximum": 100
    },
    "relics": [
      "Ash seal",
      "Ember charm"
    ],
    "potions": [
      "Smoke vial ×1"
    ],
    "chargeFlasks": [
      "HP ×2",
      "MP ×1"
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGH6: Relic rail

**Parent: WCF2.** Use cases: WGH4; separate source inventoryBelt model. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[Ash seal] [Ember charm]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH6.root | 100% usable HUD width | content-fit | owning component slot | below primary row; relics left; no potion controls | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[Ash seal] [Ember charm]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH6.root | 100% usable HUD width | content-fit | owning component slot | below primary row; relics left; no potion controls | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Ash seal] [Ember charm]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH6.root | 100% usable HUD width | content-fit | owning component slot | below primary row; relics left; no potion controls | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Ash seal] [Ember charm]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH6.root | 100% usable HUD width | content-fit | owning component slot | below primary row; relics left; no potion controls | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
// Filter layers and preserve stable entity IDs. Relics use shared cards. Potions are exclusively footer-owned: WGC11 opens WGH8 contents combining HP/MP charge providers and carried consumables. Never render any potion control in this top-HUD rail, regardless of preset. Resource meters remain distinct information components.
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/hudmeta.js:130](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hudmeta.js#L130) — Current owner checkout (including inspected local edits)
- [src/ui/models/InventoryBeltModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/InventoryBeltModel.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WC2b](wireframe-gallery.html#WC2b)

**Actual reference code:** [hud-reference-client.js](hud-reference-client.js), [hud-reference.mjs](hud-reference.mjs)

Renderer binding: `hudPlayground`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [hud-reference-client.js#hudStyleSheet](hud-reference-client.js#hudStyleSheet), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "className": "Warden",
  "cinders": 120,
  "act": 1,
  "floor": 4,
  "health": {
    "value": 32,
    "maximum": 40
  },
  "mana": {
    "value": 6,
    "maximum": 10
  },
  "stamina": {
    "value": 8,
    "maximum": 10
  },
  "experience": {
    "value": 40,
    "maximum": 100
  },
  "relics": [
    "Ash seal",
    "Ember charm"
  ],
  "potions": [
    "Smoke vial ×1"
  ],
  "chargeFlasks": [
    "HP ×2",
    "MP ×1"
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "context": "combat",
  "enabled": true,
  "layoutPreset": "proposed",
  "layers": {
    "header": true,
    "class": true,
    "cinders": true,
    "position": true,
    "vitality": true,
    "health": true,
    "mana": true,
    "stamina": true,
    "armoury": true,
    "menu": true,
    "rail": true,
    "relics": true,
    "potions": true,
    "experience": true,
    "chargeFlasks": true
  },
  "vitality": {
    "referenceMaximum": {
      "health": 200,
      "mana": 20,
      "stamina": 20
    },
    "maximumWidthPercent": 100,
    "scaleByMaximum": true
  },
  "potions": {
    "placement": "footerOnly",
    "componentId": "WGC11",
    "contentsComponentId": "WGH8",
    "openIntent": "openPotions",
    "combineChargeFlasks": true,
    "combineCarriedPotions": true
  },
  "experience": {
    "contexts": [
      "combat"
    ],
    "color": "#398bd1",
    "heightRem": 0.35,
    "animationMs": 650,
    "awardPreview": 15
  },
  "phone": {
    "gate": "data-layout='narrow'",
    "maxWidthPx": 520,
    "meters": "stacked",
    "layerOverrides": {
      "class": false,
      "position": false,
      "route": false
    }
  },
  "diagram": {
    "columns": {
      "wide": 62,
      "compact": 48,
      "portraitSE": 40,
      "portraitS24": 42
    },
    "trackColumns": 20,
    "labelColumns": 8
  },
  "layout": {
    "gapRem": 0.35,
    "insetRem": 0.5,
    "meterHeightRem": 1.15,
    "actionHeightRem": 2.75,
    "radiusRem": 0.25
  },
  "colors": {
    "background": "#211a12",
    "gold": "#d5af68",
    "text": "#eee2ca",
    "health": "#668c46",
    "mana": "#478dbc",
    "stamina": "#bf9949"
  },
  "sample": {
    "className": "Warden",
    "cinders": 120,
    "act": 1,
    "floor": 4,
    "health": {
      "value": 32,
      "maximum": 40
    },
    "mana": {
      "value": 6,
      "maximum": 10
    },
    "stamina": {
      "value": 8,
      "maximum": 10
    },
    "experience": {
      "value": 40,
      "maximum": 100
    },
    "relics": [
      "Ash seal",
      "Ember charm"
    ],
    "potions": [
      "Smoke vial ×1"
    ],
    "chargeFlasks": [
      "HP ×2",
      "MP ×1"
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGH7: Run header strip

**Parent: WCF2.** Use cases: WGH4; map/combat run header. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Warden · Cinders 120 · Act 1 Floor 4
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH7.root | 100% usable HUD width | content-fit | owning component slot | top baseline; left / center / right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Warden · Cinders 120 · Act 1 Floor 4
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH7.root | 100% usable HUD width | content-fit | owning component slot | top baseline; left / center / right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Cinders 120
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH7.root | 100% usable HUD width | content-fit | owning component slot | top baseline; left / center / right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Cinders 120
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH7.root | 100% usable HUD width | content-fit | owning component slot | top baseline; left / center / right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
// Project class, Cinders, Act and Floor from run snapshot. Filter config layers before arranging tracks. Use localization and semantic fields, not parsed text.
// UNDER config.phone.gate ONLY CINDERS REMAINS, and it keeps the middle track so it stays centred with its neighbours gone. Class, Act and Floor are not ellipsized there, they are absent — a phone is not a narrow desktop. Every context folds the same way, so the band a player reads on the map is the band they read in a fight.
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/hudmeta.js:84](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hudmeta.js#L84) — Current owner checkout (including inspected local edits)
- [src/ui/models/RunHeaderModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/RunHeaderModel.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1)

**Actual reference code:** [hud-reference-client.js](hud-reference-client.js), [hud-reference.mjs](hud-reference.mjs)

Renderer binding: `hudPlayground`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [hud-reference-client.js#hudStyleSheet](hud-reference-client.js#hudStyleSheet), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "className": "Warden",
  "cinders": 120,
  "act": 1,
  "floor": 4,
  "health": {
    "value": 32,
    "maximum": 40
  },
  "mana": {
    "value": 6,
    "maximum": 10
  },
  "stamina": {
    "value": 8,
    "maximum": 10
  },
  "experience": {
    "value": 40,
    "maximum": 100
  },
  "relics": [
    "Ash seal",
    "Ember charm"
  ],
  "potions": [
    "Smoke vial ×1"
  ],
  "chargeFlasks": [
    "HP ×2",
    "MP ×1"
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "context": "combat",
  "enabled": true,
  "layoutPreset": "proposed",
  "layers": {
    "header": true,
    "class": true,
    "cinders": true,
    "position": true,
    "vitality": true,
    "health": true,
    "mana": true,
    "stamina": true,
    "armoury": true,
    "menu": true,
    "rail": true,
    "relics": true,
    "potions": true,
    "experience": true,
    "chargeFlasks": true
  },
  "vitality": {
    "referenceMaximum": {
      "health": 200,
      "mana": 20,
      "stamina": 20
    },
    "maximumWidthPercent": 100,
    "scaleByMaximum": true
  },
  "potions": {
    "placement": "footerOnly",
    "componentId": "WGC11",
    "contentsComponentId": "WGH8",
    "openIntent": "openPotions",
    "combineChargeFlasks": true,
    "combineCarriedPotions": true
  },
  "experience": {
    "contexts": [
      "combat"
    ],
    "color": "#398bd1",
    "heightRem": 0.35,
    "animationMs": 650,
    "awardPreview": 15
  },
  "phone": {
    "gate": "data-layout='narrow'",
    "maxWidthPx": 520,
    "meters": "stacked",
    "layerOverrides": {
      "class": false,
      "position": false,
      "route": false
    }
  },
  "diagram": {
    "columns": {
      "wide": 62,
      "compact": 48,
      "portraitSE": 40,
      "portraitS24": 42
    },
    "trackColumns": 20,
    "labelColumns": 8
  },
  "layout": {
    "gapRem": 0.35,
    "insetRem": 0.5,
    "meterHeightRem": 1.15,
    "actionHeightRem": 2.75,
    "radiusRem": 0.25
  },
  "colors": {
    "background": "#211a12",
    "gold": "#d5af68",
    "text": "#eee2ca",
    "health": "#668c46",
    "mana": "#478dbc",
    "stamina": "#bf9949"
  },
  "sample": {
    "className": "Warden",
    "cinders": 120,
    "act": 1,
    "floor": 4,
    "health": {
      "value": 32,
      "maximum": 40
    },
    "mana": {
      "value": 6,
      "maximum": 10
    },
    "stamina": {
      "value": 8,
      "maximum": 10
    },
    "experience": {
      "value": 40,
      "maximum": 100
    },
    "relics": [
      "Ash seal",
      "Ember charm"
    ],
    "potions": [
      "Smoke vial ×1"
    ],
    "chargeFlasks": [
      "HP ×2",
      "MP ×1"
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGH8: Potions contents

**Parent: WCB2.** Use cases: Shared footer Potions control contents; WGC11 owns presentation. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[WGC11 Potions · footer only]
[HP ×2] [MP ×1] [Smoke vial ×1]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH8.root | shared Potions content host width | content-fit; action height config.layout.actionHeightRem | owning component slot | inside footer WGC11 disclosure; never in top HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[WGC11 Potions · footer only]
[HP ×2] [MP ×1] [Smoke vial ×1]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH8.root | shared Potions content host width | content-fit; action height config.layout.actionHeightRem | owning component slot | inside footer WGC11 disclosure; never in top HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[WGC11 Potions · footer only]
[HP ×2] [MP ×1] [Smoke vial ×1]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH8.root | shared Potions content host width | content-fit; action height config.layout.actionHeightRem | owning component slot | inside footer WGC11 disclosure; never in top HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[WGC11 Potions · footer only]
[HP ×2] [MP ×1] [Smoke vial ×1]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGH8.root | shared Potions content host width | content-fit; action height config.layout.actionHeightRem | owning component slot | inside footer WGC11 disclosure; never in top HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
// Proposed owner correction supersedes separate flask buttons.
entries = ProjectPotionEntries(snapshot.chargeFlasks, snapshot.carriedPotions, config.potions)
RenderInsideSharedControl(config.potions.componentId, entries)
// Footer-only placement applies to current and proposed preview presets.
// Data keeps charge providers separate from owned item instances; view combines references only.
OnChoose(entry): EmitRegisteredUseIntent(entry.id); DomainRevalidatesChargesAndTarget()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Current source anchors inform this proposed reference. Sample records are illustrative; this page does not execute game commands.

- [src/ui/components/hudmeta.js:109](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hudmeta.js#L109) — Current owner checkout (including inspected local edits)
- [src/ui/components/flask.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/flask.js#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB2](wireframe-gallery.html#WCB2), [W1q](wireframe-gallery.html#W1q)

**Actual reference code:** [hud-reference-client.js](hud-reference-client.js), [hud-reference.mjs](hud-reference.mjs)

Renderer binding: `hudPlayground`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [hud-reference-client.js#hudStyleSheet](hud-reference-client.js#hudStyleSheet), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "className": "Warden",
  "cinders": 120,
  "act": 1,
  "floor": 4,
  "health": {
    "value": 32,
    "maximum": 40
  },
  "mana": {
    "value": 6,
    "maximum": 10
  },
  "stamina": {
    "value": 8,
    "maximum": 10
  },
  "experience": {
    "value": 40,
    "maximum": 100
  },
  "relics": [
    "Ash seal",
    "Ember charm"
  ],
  "potions": [
    "Smoke vial ×1"
  ],
  "chargeFlasks": [
    "HP ×2",
    "MP ×1"
  ]
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "context": "combat",
  "enabled": true,
  "layoutPreset": "proposed",
  "layers": {
    "header": true,
    "class": true,
    "cinders": true,
    "position": true,
    "vitality": true,
    "health": true,
    "mana": true,
    "stamina": true,
    "armoury": true,
    "menu": true,
    "rail": true,
    "relics": true,
    "potions": true,
    "experience": true,
    "chargeFlasks": true
  },
  "vitality": {
    "referenceMaximum": {
      "health": 200,
      "mana": 20,
      "stamina": 20
    },
    "maximumWidthPercent": 100,
    "scaleByMaximum": true
  },
  "potions": {
    "placement": "footerOnly",
    "componentId": "WGC11",
    "contentsComponentId": "WGH8",
    "openIntent": "openPotions",
    "combineChargeFlasks": true,
    "combineCarriedPotions": true
  },
  "experience": {
    "contexts": [
      "combat"
    ],
    "color": "#398bd1",
    "heightRem": 0.35,
    "animationMs": 650,
    "awardPreview": 15
  },
  "phone": {
    "gate": "data-layout='narrow'",
    "maxWidthPx": 520,
    "meters": "stacked",
    "layerOverrides": {
      "class": false,
      "position": false,
      "route": false
    }
  },
  "diagram": {
    "columns": {
      "wide": 62,
      "compact": 48,
      "portraitSE": 40,
      "portraitS24": 42
    },
    "trackColumns": 20,
    "labelColumns": 8
  },
  "layout": {
    "gapRem": 0.35,
    "insetRem": 0.5,
    "meterHeightRem": 1.15,
    "actionHeightRem": 2.75,
    "radiusRem": 0.25
  },
  "colors": {
    "background": "#211a12",
    "gold": "#d5af68",
    "text": "#eee2ca",
    "health": "#668c46",
    "mana": "#478dbc",
    "stamina": "#bf9949"
  },
  "sample": {
    "className": "Warden",
    "cinders": 120,
    "act": 1,
    "floor": 4,
    "health": {
      "value": 32,
      "maximum": 40
    },
    "mana": {
      "value": 6,
      "maximum": 10
    },
    "stamina": {
      "value": 8,
      "maximum": 10
    },
    "experience": {
      "value": 40,
      "maximum": 100
    },
    "relics": [
      "Ash seal",
      "Ember charm"
    ],
    "potions": [
      "Smoke vial ×1"
    ],
    "chargeFlasks": [
      "HP ×2",
      "MP ×1"
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGM0: Map composition

**Parent: none.** Use cases: Map composition reusable component family. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Shared HUD
Region [Ashen March ▾]
Map viewport: connected node graph
Selected node: known details
[Recenter]                 [Enter town]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Shared HUD
Region [Ashen March ▾]
Map viewport: connected node graph
Selected node: known details
[Recenter]                 [Enter town]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Shared HUD
Region [Ashen March ▾]
Map viewport: connected node graph
Selected node: known details
[Recenter]                 [Enter town]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Shared HUD
Region [Ashen March ▾]
Map viewport: connected node graph
Selected node: known details
[Recenter]                 [Enter town]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Compose shared HUD, region selector, camera viewport, selected-node details and inline footer. Use configured map bands; node-selection callback updates details and entry readiness without entering immediately. Region selection is separate from camera graph contents.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the map source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/mapboard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/mapboard.js#L1) — Current owner checkout (including inspected local edits)
- [styles/map.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/map.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGH4](wireframe-gallery.html#WGH4), [WGM5](wireframe-gallery.html#WGM5), [WGM1](wireframe-gallery.html#WGM1), [WGM4](wireframe-gallery.html#WGM4), [WGM6](wireframe-gallery.html#WGM6), [WGM7](wireframe-gallery.html#WGM7)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGM0",
  "contextRef": "sample:map",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WGH4",
      "modelRef": "model:WGH4"
    },
    {
      "componentId": "WGM5",
      "modelRef": "model:WGM5"
    },
    {
      "componentId": "WGM1",
      "modelRef": "model:WGM1"
    },
    {
      "componentId": "WGM4",
      "modelRef": "model:WGM4"
    },
    {
      "componentId": "WGM6",
      "modelRef": "model:WGM6"
    },
    {
      "componentId": "WGM7",
      "modelRef": "model:WGM7"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGM1: Map viewport

**Parent: WCF2.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
         [?]
        /   \
   [Combat] [Town]
        \   /
       [Visited]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM1.root | config.mapWidth | config.scene | owning component slot | below HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
         [?]
        /   \
   [Combat] [Town]
        \   /
       [Visited]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM1.root | config.mapWidth | config.scene | owning component slot | below HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
         [?]
        /   \
   [Combat] [Town]
        \   /
       [Visited]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM1.root | config.mapWidth | config.scene | owning component slot | below HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
         [?]
        /   \
   [Combat] [Town]
        \   /
       [Visited]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM1.root | config.mapWidth | config.scene | owning component slot | below HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Render graph paths and buttons in a shared camera coordinate space. Preserve node identity during pan/zoom. Apply knowledge filtering before rendering unknown node labels. Publish selected-node model to the WGM4 details host; the sibling WGM5 owns region selection.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the map source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/mapboard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/mapboard.js#L1) — Current owner checkout (including inspected local edits)
- [styles/map.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/map.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGM2](wireframe-gallery.html#WGM2), [WGM3](wireframe-gallery.html#WGM3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGM1",
  "contextRef": "sample:map",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WGM2",
      "modelRef": "model:WGM2"
    },
    {
      "componentId": "WGM3",
      "modelRef": "model:WGM3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGM2: Map paths

**Parent: WGS1.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
      ●
     / \
    ●   ●
     \ /
      ●
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM2.root | map bounds | map bounds | owning component slot | inside viewport | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
      ●
     / \
    ●   ●
     \ /
      ●
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM2.root | map bounds | map bounds | owning component slot | inside viewport | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
      ●
     / \
    ●   ●
     \ /
      ●
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM2.root | map bounds | map bounds | owning component slot | inside viewport | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
      ●
     / \
    ●   ●
     \ /
      ●
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM2.root | map bounds | map bounds | owning component slot | inside viewport | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Read graph edges and registered node positions. Render noninteractive SVG paths beneath buttons; expose graph connections in accessible node descriptions.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the map source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/mapboard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/mapboard.js#L1) — Current owner checkout (including inspected local edits)
- [styles/map.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/map.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGM2",
  "contextRef": "sample:map",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGM3: Node button

**Parent: WCB3.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Visited ●   Reachable [⌂]   Blocked [?]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM3.root | target token | target token | owning component slot | authored graph coordinate | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Visited ●   Reachable [⌂]   Blocked [?]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM3.root | target token | target token | owning component slot | authored graph coordinate | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Visited ●   Reachable [⌂]   Blocked [?]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM3.root | target token | target token | owning component slot | authored graph coordinate | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Visited ●   Reachable [⌂]   Blocked [?]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM3.root | target token | target token | owning component slot | authored graph coordinate | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Render node state from knowledge-filtered graph. Selection only updates preview state. Blocked nodes explain unavailable reason without permitting entry.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the map source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/mapboard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/mapboard.js#L1) — Current owner checkout (including inspected local edits)
- [styles/map.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/map.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGM3",
  "contextRef": "sample:map",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB3",
      "modelRef": "model:WCB3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGM4: Node details

**Parent: WCF4.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Town · Reachable
Services   Smith · Merchant · Rest
Risk       Known safe
Entry      Select Enter town to travel
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM4.root | 100% | config.context | owning component slot | below map | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Town · Reachable
Services   Smith · Merchant · Rest
Risk       Known safe
Entry      Select Enter town to travel
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM4.root | 100% | config.context | owning component slot | below map | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Town · Reachable
Services   Smith · Merchant · Rest
Risk       Known safe
Entry      Select Enter town to travel
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM4.root | 100% | config.context | owning component slot | below map | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Town · Reachable
Services   Smith · Merchant · Rest
Risk       Known safe
Entry      Select Enter town to travel
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM4.root | 100% | config.context | owning component slot | below map | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Project known selected-node identity, services, risk and entry reason. Use aligned label/value rows. Do not disclose unseen or unprovided node data.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the map source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/mapboard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/mapboard.js#L1) — Current owner checkout (including inspected local edits)
- [styles/map.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/map.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCF4](wireframe-gallery.html#WCF4)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGM4",
  "contextRef": "sample:map",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCF4",
      "modelRef": "model:WCF4"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGM5: Region selector

**Parent: WCB3.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Region [Ashen March ▾]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM5.root | content-fit | target token | owning component slot | inside HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Region [Ashen March ▾]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM5.root | content-fit | target token | owning component slot | inside HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Region [Ashen March ▾]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM5.root | content-fit | target token | owning component slot | inside HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Region [Ashen March ▾]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM5.root | content-fit | target token | owning component slot | inside HUD | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Populate available regions from model. Selecting changes the local map view while preserving run location; no travel command is emitted.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the map source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/mapboard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/mapboard.js#L1) — Current owner checkout (including inspected local edits)
- [styles/map.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/map.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGM5",
  "contextRef": "sample:map",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGM6: Recenter button

**Parent: WCB4.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[Recenter]                 [Enter town]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM6.root | action width | target token | owning component slot | footer left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[Recenter]                 [Enter town]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM6.root | action width | target token | owning component slot | footer left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Recenter]                 [Enter town]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM6.root | action width | target token | owning component slot | footer left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Recenter]                 [Enter town]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM6.root | action width | target token | owning component slot | footer left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Emit local camera recenter intent for current node. Reset view transform without changing selected destination or run graph.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the map source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/mapboard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/mapboard.js#L1) — Current owner checkout (including inspected local edits)
- [styles/map.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/map.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGM6",
  "contextRef": "sample:map",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB4",
      "modelRef": "model:WCB4"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGM7: Enter node button

**Parent: WCB3.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[Recenter]                 [Enter town]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM7.root | action width | target token | owning component slot | footer right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[Recenter]                 [Enter town]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM7.root | action width | target token | owning component slot | footer right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Recenter]                 [Enter town]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM7.root | action width | target token | owning component slot | footer right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Recenter]                 [Enter town]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGM7.root | action width | target token | owning component slot | footer right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Project node-specific label and readiness. Revalidate reachability and entry command against latest domain state before transition.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the map source boundary. Reference sample data and interaction styling are proposed; source modules establish ownership and behavior.

- [src/ui/components/mapboard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/mapboard.js#L1) — Current owner checkout (including inspected local edits)
- [styles/map.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/map.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGM7",
  "contextRef": "sample:map",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB3",
      "modelRef": "model:WCB3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGP0: Progression components

**Parent: WCF0.** Use cases: Shared weapon and skill progression. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[WGP1 list] → [WGP2 progress]
               [WGP4 next benefit]
               [WGP3 techniques]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP0.root | available width | content fit | owning component slot | W1 active body | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[WGP1 list] → [WGP2 progress]
               [WGP4 next benefit]
               [WGP3 techniques]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP0.root | available width | content fit | owning component slot | W1 active body | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[WGP1 list] → [WGP2 progress]
               [WGP4 next benefit]
               [WGP3 techniques]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP0.root | available width | content fit | owning component slot | W1 active body | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[WGP1 list] → [WGP2 progress]
               [WGP4 next benefit]
               [WGP3 techniques]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP0.root | available width | content fit | owning component slot | W1 active body | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT projection, config.progression
// Children consume the same selected proficiency ID.
ComposeRegisteredChildren(projection, config.progression.layout)
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

The existing equipment screen, equipment cards, modal shell and meters are source references for presentation reuse. Per-weapon and per-skill practice progression is proposed; these sample ranks, names and awards are illustrative, not shipped mechanics.

- [src/ui/screens/equipment.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/equipment.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/resbars.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/resbars.js#L1) — Current owner checkout (including inspected local edits)
- [PROGRESSION-SPECIFICATION.md](PROGRESSION-SPECIFICATION.md) — Proposed reference specification

**Referenced components:** [WGP1](wireframe-gallery.html#WGP1), [WGP2](wireframe-gallery.html#WGP2), [WGP3](wireframe-gallery.html#WGP3), [WGP4](wireframe-gallery.html#WGP4), [WCB4](wireframe-gallery.html#WCB4), [WCB5](wireframe-gallery.html#WCB5)

**Actual reference code:** [progression-reference-client.js](progression-reference-client.js), [progression-reference.mjs](progression-reference.mjs)

Renderer binding: `renderProgression`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [progression-reference.css](progression-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
[
  {
    "id": "swords",
    "category": "weapons",
    "name": "Swords",
    "rank": 3,
    "progress": 42,
    "required": 100,
    "description": "Practice with sword techniques.",
    "next": "Next sword technique becomes available for review.",
    "techniques": [
      {
        "name": "Measured cut",
        "known": true
      },
      {
        "name": "Guard break",
        "known": false
      }
    ],
    "history": [
      "Last encounter: sword practice recorded."
    ]
  },
  {
    "id": "bows",
    "category": "weapons",
    "name": "Bows",
    "rank": 2,
    "progress": 68,
    "required": 100,
    "description": "Practice with aimed attacks.",
    "next": "Next bow technique becomes available for review.",
    "techniques": [
      {
        "name": "Steady aim",
        "known": true
      },
      {
        "name": "Piercing shot",
        "known": false
      }
    ],
    "history": [
      "Last encounter: bow practice recorded."
    ]
  },
  {
    "id": "restoration",
    "category": "skills",
    "name": "Restoration",
    "rank": 2,
    "progress": 35,
    "required": 100,
    "description": "Practice with restorative skills.",
    "next": "Next restoration technique becomes available for review.",
    "techniques": [
      {
        "name": "Mend",
        "known": true
      },
      {
        "name": "Renew",
        "known": false
      }
    ],
    "history": [
      "Last encounter: restoration practice recorded."
    ]
  },
  {
    "id": "guarding",
    "category": "skills",
    "name": "Guarding",
    "rank": 1,
    "progress": 80,
    "required": 100,
    "description": "Practice with defensive skills.",
    "next": "Next defensive technique becomes available for review.",
    "techniques": [
      {
        "name": "Brace",
        "known": true
      },
      {
        "name": "Counterguard",
        "known": false
      }
    ],
    "history": [
      "Last encounter: defensive practice recorded."
    ]
  }
]
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "categories": [
    "weapons",
    "skills"
  ],
  "categoryIds": {
    "weapons": "weapons",
    "skills": "skills"
  },
  "defaultCategory": "weapons",
  "showLockedTechniques": true,
  "showHistory": true,
  "allowPreviewAward": true,
  "previewAward": 12,
  "progressMinimum": 0,
  "progressMaximum": 100,
  "labels": {
    "title": "Proficiencies",
    "back": "Back",
    "award": "Preview progress"
  },
  "layout": {
    "width": "min(95vw, 68rem)",
    "height": "min(90vh, 48rem)",
    "inset": "1rem",
    "gap": ".75rem",
    "sidebar": "minmax(0, 1fr)",
    "list": "minmax(0, 1fr)",
    "detail": "minmax(0, 2fr)",
    "meterHeight": ".6rem",
    "compactBreakpoint": "42rem",
    "standardButtonHeight": "2.75rem",
    "tileHeightMultiplier": 2,
    "categoryButtonSize": "full-standard",
    "tileButtonSize": "full-double"
  },
  "colors": {
    "background": "#241c13",
    "inset": "#19140e",
    "text": "#efe5d4",
    "muted": "#c1ad8b",
    "gold": "#d1aa60",
    "progress": "#4189b6",
    "primary": "#557b3f",
    "danger": "#ac4e3c"
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGP1: Proficiency list row

**Parent: WCF1.** Use cases: Weapon and skill navigation; links WCI1, WGP2; same tile contract in every profile. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
┌───────────────────────────────┐
│ Name                   Rank 3 │
│ [████░░░░░░░░]         42/100 │
└───────────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP1.root | available list-column width, uniform across all tiles | config.progression.layout.standardButtonHeight * config.progression.layout.tileHeightMultiplier | owning component slot | top aligned; name inline-start and rank inline-end; progress directly below | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
┌───────────────────────────────┐
│ Name                   Rank 3 │
│ [████░░░░░░░░]         42/100 │
└───────────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP1.root | available list-column width, uniform across all tiles | config.progression.layout.standardButtonHeight * config.progression.layout.tileHeightMultiplier | owning component slot | top aligned; name inline-start and rank inline-end; progress directly below | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────────┐
│ Name                   Rank 3 │
│ [████░░░░░░░░]         42/100 │
└───────────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP1.root | available list-column width, uniform across all tiles | config.progression.layout.standardButtonHeight * config.progression.layout.tileHeightMultiplier | owning component slot | top aligned; name inline-start and rank inline-end; progress directly below | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌───────────────────────────────┐
│ Name                   Rank 3 │
│ [████░░░░░░░░]         42/100 │
└───────────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP1.root | available list-column width, uniform across all tiles | config.progression.layout.standardButtonHeight * config.progression.layout.tileHeightMultiplier | owning component slot | top aligned; name inline-start and rank inline-end; progress directly below | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT proficiency summary, selected ID, config.progression
// Preserve stable selection across categories and host sizes.
tileHeight = config.progression.layout.standardButtonHeight * config.progression.layout.tileHeightMultiplier
SetUniformTileSize(width=AvailableListColumnWidth(), height=tileHeight)
RenderInlineNameAndRank(WCI1, summary, alignName=InlineStart(), alignRank=InlineEnd())
// The next row is a single inline meter/value component, with no extra subtitle.
ComposeBelow(WGP2, summary)
OnActivate: SelectProficiency(summary.id)
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

The existing equipment screen, equipment cards, modal shell and meters are source references for presentation reuse. Per-weapon and per-skill practice progression is proposed; these sample ranks, names and awards are illustrative, not shipped mechanics.

- [src/ui/screens/equipment.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/equipment.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/resbars.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/resbars.js#L1) — Current owner checkout (including inspected local edits)
- [PROGRESSION-SPECIFICATION.md](PROGRESSION-SPECIFICATION.md) — Proposed reference specification

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1), [WGP2](wireframe-gallery.html#WGP2)

**Actual reference code:** [progression-reference-client.js](progression-reference-client.js), [progression-reference.mjs](progression-reference.mjs)

Renderer binding: `renderProgression`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [progression-reference.css](progression-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
[
  {
    "id": "swords",
    "category": "weapons",
    "name": "Swords",
    "rank": 3,
    "progress": 42,
    "required": 100,
    "description": "Practice with sword techniques.",
    "next": "Next sword technique becomes available for review.",
    "techniques": [
      {
        "name": "Measured cut",
        "known": true
      },
      {
        "name": "Guard break",
        "known": false
      }
    ],
    "history": [
      "Last encounter: sword practice recorded."
    ]
  },
  {
    "id": "bows",
    "category": "weapons",
    "name": "Bows",
    "rank": 2,
    "progress": 68,
    "required": 100,
    "description": "Practice with aimed attacks.",
    "next": "Next bow technique becomes available for review.",
    "techniques": [
      {
        "name": "Steady aim",
        "known": true
      },
      {
        "name": "Piercing shot",
        "known": false
      }
    ],
    "history": [
      "Last encounter: bow practice recorded."
    ]
  },
  {
    "id": "restoration",
    "category": "skills",
    "name": "Restoration",
    "rank": 2,
    "progress": 35,
    "required": 100,
    "description": "Practice with restorative skills.",
    "next": "Next restoration technique becomes available for review.",
    "techniques": [
      {
        "name": "Mend",
        "known": true
      },
      {
        "name": "Renew",
        "known": false
      }
    ],
    "history": [
      "Last encounter: restoration practice recorded."
    ]
  },
  {
    "id": "guarding",
    "category": "skills",
    "name": "Guarding",
    "rank": 1,
    "progress": 80,
    "required": 100,
    "description": "Practice with defensive skills.",
    "next": "Next defensive technique becomes available for review.",
    "techniques": [
      {
        "name": "Brace",
        "known": true
      },
      {
        "name": "Counterguard",
        "known": false
      }
    ],
    "history": [
      "Last encounter: defensive practice recorded."
    ]
  }
]
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "categories": [
    "weapons",
    "skills"
  ],
  "categoryIds": {
    "weapons": "weapons",
    "skills": "skills"
  },
  "defaultCategory": "weapons",
  "showLockedTechniques": true,
  "showHistory": true,
  "allowPreviewAward": true,
  "previewAward": 12,
  "progressMinimum": 0,
  "progressMaximum": 100,
  "labels": {
    "title": "Proficiencies",
    "back": "Back",
    "award": "Preview progress"
  },
  "layout": {
    "width": "min(95vw, 68rem)",
    "height": "min(90vh, 48rem)",
    "inset": "1rem",
    "gap": ".75rem",
    "sidebar": "minmax(0, 1fr)",
    "list": "minmax(0, 1fr)",
    "detail": "minmax(0, 2fr)",
    "meterHeight": ".6rem",
    "compactBreakpoint": "42rem",
    "standardButtonHeight": "2.75rem",
    "tileHeightMultiplier": 2,
    "categoryButtonSize": "full-standard",
    "tileButtonSize": "full-double"
  },
  "colors": {
    "background": "#241c13",
    "inset": "#19140e",
    "text": "#efe5d4",
    "muted": "#c1ad8b",
    "gold": "#d1aa60",
    "progress": "#4189b6",
    "primary": "#557b3f",
    "danger": "#ac4e3c"
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGP2: Proficiency progress

**Parent: WCM2.** Use cases: List rows, details and award preview; same inline meter/value layout in wide, compact, SE and S24. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[████████░░░░░░░░]  42/100
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP2.root | available row width; meter takes remaining width after fraction and shared gap | row content-fit; bar thickness=config.progression.layout.meterHeight | owning component slot | meter inline-start and fraction inline-end; vertically centered within the same row | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[████████░░░░░░░░]  42/100
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP2.root | available row width; meter takes remaining width after fraction and shared gap | row content-fit; bar thickness=config.progression.layout.meterHeight | owning component slot | meter inline-start and fraction inline-end; vertically centered within the same row | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[████████░░░░░░░░]  42/100
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP2.root | available row width; meter takes remaining width after fraction and shared gap | row content-fit; bar thickness=config.progression.layout.meterHeight | owning component slot | meter inline-start and fraction inline-end; vertically centered within the same row | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[████████░░░░░░░░]  42/100
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP2.root | available row width; meter takes remaining width after fraction and shared gap | row content-fit; bar thickness=config.progression.layout.meterHeight | owning component slot | meter inline-start and fraction inline-end; vertically centered within the same row | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT accumulated practice, threshold, config.progression
// Domain projection supplies values; renderer does not award progression.
fraction = SafeProgressFraction(accumulatedPractice, threshold)
ComposeInlineRow(gap=config.progression.layout.gap)
RenderSharedMeter(WCM2, fraction, config.progression.colors.progress, thickness=config.progression.layout.meterHeight, width=RemainingRowWidth())
RenderFractionAtInlineEnd(accumulatedPractice, threshold)
// No Practice subtitle: the fraction and accessible label already identify progress.
ProvideAccessibleValueText(accumulatedPractice, threshold)
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

The existing equipment screen, equipment cards, modal shell and meters are source references for presentation reuse. Per-weapon and per-skill practice progression is proposed; these sample ranks, names and awards are illustrative, not shipped mechanics.

- [src/ui/screens/equipment.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/equipment.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/resbars.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/resbars.js#L1) — Current owner checkout (including inspected local edits)
- [PROGRESSION-SPECIFICATION.md](PROGRESSION-SPECIFICATION.md) — Proposed reference specification

**Referenced components:** [WCM2](wireframe-gallery.html#WCM2)

**Actual reference code:** [progression-reference-client.js](progression-reference-client.js), [progression-reference.mjs](progression-reference.mjs)

Renderer binding: `renderProgression`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [progression-reference.css](progression-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
[
  {
    "id": "swords",
    "category": "weapons",
    "name": "Swords",
    "rank": 3,
    "progress": 42,
    "required": 100,
    "description": "Practice with sword techniques.",
    "next": "Next sword technique becomes available for review.",
    "techniques": [
      {
        "name": "Measured cut",
        "known": true
      },
      {
        "name": "Guard break",
        "known": false
      }
    ],
    "history": [
      "Last encounter: sword practice recorded."
    ]
  },
  {
    "id": "bows",
    "category": "weapons",
    "name": "Bows",
    "rank": 2,
    "progress": 68,
    "required": 100,
    "description": "Practice with aimed attacks.",
    "next": "Next bow technique becomes available for review.",
    "techniques": [
      {
        "name": "Steady aim",
        "known": true
      },
      {
        "name": "Piercing shot",
        "known": false
      }
    ],
    "history": [
      "Last encounter: bow practice recorded."
    ]
  },
  {
    "id": "restoration",
    "category": "skills",
    "name": "Restoration",
    "rank": 2,
    "progress": 35,
    "required": 100,
    "description": "Practice with restorative skills.",
    "next": "Next restoration technique becomes available for review.",
    "techniques": [
      {
        "name": "Mend",
        "known": true
      },
      {
        "name": "Renew",
        "known": false
      }
    ],
    "history": [
      "Last encounter: restoration practice recorded."
    ]
  },
  {
    "id": "guarding",
    "category": "skills",
    "name": "Guarding",
    "rank": 1,
    "progress": 80,
    "required": 100,
    "description": "Practice with defensive skills.",
    "next": "Next defensive technique becomes available for review.",
    "techniques": [
      {
        "name": "Brace",
        "known": true
      },
      {
        "name": "Counterguard",
        "known": false
      }
    ],
    "history": [
      "Last encounter: defensive practice recorded."
    ]
  }
]
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "categories": [
    "weapons",
    "skills"
  ],
  "categoryIds": {
    "weapons": "weapons",
    "skills": "skills"
  },
  "defaultCategory": "weapons",
  "showLockedTechniques": true,
  "showHistory": true,
  "allowPreviewAward": true,
  "previewAward": 12,
  "progressMinimum": 0,
  "progressMaximum": 100,
  "labels": {
    "title": "Proficiencies",
    "back": "Back",
    "award": "Preview progress"
  },
  "layout": {
    "width": "min(95vw, 68rem)",
    "height": "min(90vh, 48rem)",
    "inset": "1rem",
    "gap": ".75rem",
    "sidebar": "minmax(0, 1fr)",
    "list": "minmax(0, 1fr)",
    "detail": "minmax(0, 2fr)",
    "meterHeight": ".6rem",
    "compactBreakpoint": "42rem",
    "standardButtonHeight": "2.75rem",
    "tileHeightMultiplier": 2,
    "categoryButtonSize": "full-standard",
    "tileButtonSize": "full-double"
  },
  "colors": {
    "background": "#241c13",
    "inset": "#19140e",
    "text": "#efe5d4",
    "muted": "#c1ad8b",
    "gold": "#d1aa60",
    "progress": "#4189b6",
    "primary": "#557b3f",
    "danger": "#ac4e3c"
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGP3: Known techniques

**Parent: WCF4.** Use cases: Knowledge-filtered proficiency details. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Known techniques
Measured cut       Available
Guard break        Undiscovered
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP3.root | available detail width | content fit | owning component slot | aligned label/value columns | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Known techniques
Measured cut       Available
Guard break        Undiscovered
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP3.root | available detail width | content fit | owning component slot | aligned label/value columns | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Known techniques
Measured cut       Available
Guard break        Undiscovered
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP3.root | available detail width | content fit | owning component slot | aligned label/value columns | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Known techniques
Measured cut       Available
Guard break        Undiscovered
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP3.root | available detail width | content fit | owning component slot | aligned label/value columns | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT technique unlock projection, knowledge, config.progression
// Unknown mechanics must not leak through tooltips or source-backed view data.
rows = ApplyKnowledgeFilter(techniques, knowledge)
If config.progression.showLockedTechniques: AppendGenericUnknownRows()
RenderFactRows(WCF4, rows)
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

The existing equipment screen, equipment cards, modal shell and meters are source references for presentation reuse. Per-weapon and per-skill practice progression is proposed; these sample ranks, names and awards are illustrative, not shipped mechanics.

- [src/ui/screens/equipment.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/equipment.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/resbars.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/resbars.js#L1) — Current owner checkout (including inspected local edits)
- [PROGRESSION-SPECIFICATION.md](PROGRESSION-SPECIFICATION.md) — Proposed reference specification

**Referenced components:** [WCF4](wireframe-gallery.html#WCF4)

**Actual reference code:** [progression-reference-client.js](progression-reference-client.js), [progression-reference.mjs](progression-reference.mjs)

Renderer binding: `renderProgression`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [progression-reference.css](progression-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
[
  {
    "id": "swords",
    "category": "weapons",
    "name": "Swords",
    "rank": 3,
    "progress": 42,
    "required": 100,
    "description": "Practice with sword techniques.",
    "next": "Next sword technique becomes available for review.",
    "techniques": [
      {
        "name": "Measured cut",
        "known": true
      },
      {
        "name": "Guard break",
        "known": false
      }
    ],
    "history": [
      "Last encounter: sword practice recorded."
    ]
  },
  {
    "id": "bows",
    "category": "weapons",
    "name": "Bows",
    "rank": 2,
    "progress": 68,
    "required": 100,
    "description": "Practice with aimed attacks.",
    "next": "Next bow technique becomes available for review.",
    "techniques": [
      {
        "name": "Steady aim",
        "known": true
      },
      {
        "name": "Piercing shot",
        "known": false
      }
    ],
    "history": [
      "Last encounter: bow practice recorded."
    ]
  },
  {
    "id": "restoration",
    "category": "skills",
    "name": "Restoration",
    "rank": 2,
    "progress": 35,
    "required": 100,
    "description": "Practice with restorative skills.",
    "next": "Next restoration technique becomes available for review.",
    "techniques": [
      {
        "name": "Mend",
        "known": true
      },
      {
        "name": "Renew",
        "known": false
      }
    ],
    "history": [
      "Last encounter: restoration practice recorded."
    ]
  },
  {
    "id": "guarding",
    "category": "skills",
    "name": "Guarding",
    "rank": 1,
    "progress": 80,
    "required": 100,
    "description": "Practice with defensive skills.",
    "next": "Next defensive technique becomes available for review.",
    "techniques": [
      {
        "name": "Brace",
        "known": true
      },
      {
        "name": "Counterguard",
        "known": false
      }
    ],
    "history": [
      "Last encounter: defensive practice recorded."
    ]
  }
]
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "categories": [
    "weapons",
    "skills"
  ],
  "categoryIds": {
    "weapons": "weapons",
    "skills": "skills"
  },
  "defaultCategory": "weapons",
  "showLockedTechniques": true,
  "showHistory": true,
  "allowPreviewAward": true,
  "previewAward": 12,
  "progressMinimum": 0,
  "progressMaximum": 100,
  "labels": {
    "title": "Proficiencies",
    "back": "Back",
    "award": "Preview progress"
  },
  "layout": {
    "width": "min(95vw, 68rem)",
    "height": "min(90vh, 48rem)",
    "inset": "1rem",
    "gap": ".75rem",
    "sidebar": "minmax(0, 1fr)",
    "list": "minmax(0, 1fr)",
    "detail": "minmax(0, 2fr)",
    "meterHeight": ".6rem",
    "compactBreakpoint": "42rem",
    "standardButtonHeight": "2.75rem",
    "tileHeightMultiplier": 2,
    "categoryButtonSize": "full-standard",
    "tileButtonSize": "full-double"
  },
  "colors": {
    "background": "#241c13",
    "inset": "#19140e",
    "text": "#efe5d4",
    "muted": "#c1ad8b",
    "gold": "#d1aa60",
    "progress": "#4189b6",
    "primary": "#557b3f",
    "danger": "#ac4e3c"
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGP4: Next proficiency benefit

**Parent: WCF4.** Use cases: Weapon and skill progression details. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Next benefit
{Known benefit or undiscovered}
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP4.root | available detail width | content fit | owning component slot | start aligned in detail stack | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Next benefit
{Known benefit or undiscovered}
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP4.root | available detail width | content fit | owning component slot | start aligned in detail stack | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Next benefit
{Known benefit or undiscovered}
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP4.root | available detail width | content fit | owning component slot | start aligned in detail stack | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Next benefit
{Known benefit or undiscovered}
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGP4.root | available detail width | content fit | owning component slot | start aligned in detail stack | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT next threshold projection, knowledge, config.progression
// Threshold effects are data references; avoid invented growth formulas.
benefit = ProjectKnownBenefitOrUnknown(nextThreshold, knowledge)
RenderFactRows(WCF4, benefit)
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

The existing equipment screen, equipment cards, modal shell and meters are source references for presentation reuse. Per-weapon and per-skill practice progression is proposed; these sample ranks, names and awards are illustrative, not shipped mechanics.

- [src/ui/screens/equipment.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/equipment.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/equipmentCard.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/equipmentCard.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/modalShell.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/modalShell.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/resbars.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/resbars.js#L1) — Current owner checkout (including inspected local edits)
- [PROGRESSION-SPECIFICATION.md](PROGRESSION-SPECIFICATION.md) — Proposed reference specification

**Referenced components:** [WCF4](wireframe-gallery.html#WCF4)

**Actual reference code:** [progression-reference-client.js](progression-reference-client.js), [progression-reference.mjs](progression-reference.mjs)

Renderer binding: `renderProgression`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [progression-reference.css](progression-reference.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
[
  {
    "id": "swords",
    "category": "weapons",
    "name": "Swords",
    "rank": 3,
    "progress": 42,
    "required": 100,
    "description": "Practice with sword techniques.",
    "next": "Next sword technique becomes available for review.",
    "techniques": [
      {
        "name": "Measured cut",
        "known": true
      },
      {
        "name": "Guard break",
        "known": false
      }
    ],
    "history": [
      "Last encounter: sword practice recorded."
    ]
  },
  {
    "id": "bows",
    "category": "weapons",
    "name": "Bows",
    "rank": 2,
    "progress": 68,
    "required": 100,
    "description": "Practice with aimed attacks.",
    "next": "Next bow technique becomes available for review.",
    "techniques": [
      {
        "name": "Steady aim",
        "known": true
      },
      {
        "name": "Piercing shot",
        "known": false
      }
    ],
    "history": [
      "Last encounter: bow practice recorded."
    ]
  },
  {
    "id": "restoration",
    "category": "skills",
    "name": "Restoration",
    "rank": 2,
    "progress": 35,
    "required": 100,
    "description": "Practice with restorative skills.",
    "next": "Next restoration technique becomes available for review.",
    "techniques": [
      {
        "name": "Mend",
        "known": true
      },
      {
        "name": "Renew",
        "known": false
      }
    ],
    "history": [
      "Last encounter: restoration practice recorded."
    ]
  },
  {
    "id": "guarding",
    "category": "skills",
    "name": "Guarding",
    "rank": 1,
    "progress": 80,
    "required": 100,
    "description": "Practice with defensive skills.",
    "next": "Next defensive technique becomes available for review.",
    "techniques": [
      {
        "name": "Brace",
        "known": true
      },
      {
        "name": "Counterguard",
        "known": false
      }
    ],
    "history": [
      "Last encounter: defensive practice recorded."
    ]
  }
]
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "categories": [
    "weapons",
    "skills"
  ],
  "categoryIds": {
    "weapons": "weapons",
    "skills": "skills"
  },
  "defaultCategory": "weapons",
  "showLockedTechniques": true,
  "showHistory": true,
  "allowPreviewAward": true,
  "previewAward": 12,
  "progressMinimum": 0,
  "progressMaximum": 100,
  "labels": {
    "title": "Proficiencies",
    "back": "Back",
    "award": "Preview progress"
  },
  "layout": {
    "width": "min(95vw, 68rem)",
    "height": "min(90vh, 48rem)",
    "inset": "1rem",
    "gap": ".75rem",
    "sidebar": "minmax(0, 1fr)",
    "list": "minmax(0, 1fr)",
    "detail": "minmax(0, 2fr)",
    "meterHeight": ".6rem",
    "compactBreakpoint": "42rem",
    "standardButtonHeight": "2.75rem",
    "tileHeightMultiplier": 2,
    "categoryButtonSize": "full-standard",
    "tileButtonSize": "full-double"
  },
  "colors": {
    "background": "#241c13",
    "inset": "#19140e",
    "text": "#efe5d4",
    "muted": "#c1ad8b",
    "gold": "#d1aa60",
    "progress": "#4189b6",
    "primary": "#557b3f",
    "danger": "#ac4e3c"
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGQ0: Dialogue composition

**Parent: none.** Use cases: Dialogue composition reusable component family. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Player portrait      NPC portrait
Speaker: short current caption
[Available authored choices]
[Back]       [Skip speech]       [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Player portrait      NPC portrait
Speaker: short current caption
[Available authored choices]
[Back]       [Skip speech]       [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Player portrait      NPC portrait
Speaker: short current caption
[Available authored choices]
[Back]       [Skip speech]       [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Player portrait      NPC portrait
Speaker: short current caption
[Available authored choices]
[Back]       [Skip speech]       [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Compose scene portraits, current authored beat, progression controller and inline navigation. Keep quest effects behind explicit domain choice commit.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the dialogue source boundary. Current event screen renders authored prose and domain-backed choices. Two portraits and audio-paced caption progression are proposed; sample text is illustrative.

- [src/ui/screens/event.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/quests.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/quests.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/optionDecision.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/optionDecision.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGQ1](wireframe-gallery.html#WGQ1), [WGQ4](wireframe-gallery.html#WGQ4), [WGQ5](wireframe-gallery.html#WGQ5), [WGQ6](wireframe-gallery.html#WGQ6), [WGQ7](wireframe-gallery.html#WGQ7), [WGQ8](wireframe-gallery.html#WGQ8)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGQ0",
  "contextRef": "sample:dialogue",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WGQ1",
      "modelRef": "model:WGQ1"
    },
    {
      "componentId": "WGQ4",
      "modelRef": "model:WGQ4"
    },
    {
      "componentId": "WGQ5",
      "modelRef": "model:WGQ5"
    },
    {
      "componentId": "WGQ6",
      "modelRef": "model:WGQ6"
    },
    {
      "componentId": "WGQ7",
      "modelRef": "model:WGQ7"
    },
    {
      "componentId": "WGQ8",
      "modelRef": "model:WGQ8"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGQ1: Dialogue scene

**Parent: WGS1.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
┌─────── authored scene ─────────┐
│ Player →             ← Keeper │
│ portrait              portrait│
└───────────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ1.root | 100% | frame height; scene window = config.bands.scene | owning component slot | frame, behind every band | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
┌─────── authored scene ─────────┐
│ Player →             ← Keeper │
│ portrait              portrait│
└───────────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ1.root | 100% | frame height; scene window = config.bands.scene | owning component slot | frame, behind every band | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌─────── authored scene ─────────┐
│ Player →             ← Keeper │
│ portrait              portrait│
└───────────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ1.root | 100% | frame height; scene window = config.bands.scene | owning component slot | frame, behind every band | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌─────── authored scene ─────────┐
│ Player →             ← Keeper │
│ portrait              portrait│
└───────────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ1.root | 100% | frame height; scene window = config.bands.scene | owning component slot | frame, behind every band | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Resolve authored scene and speaker portraits. Preserve left/right roles and sprite proportions; apply speaking emphasis only to the current speaker.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the dialogue source boundary. Current event screen renders authored prose and domain-backed choices. Two portraits and audio-paced caption progression are proposed; sample text is illustrative.

- [src/ui/screens/event.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/quests.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/quests.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/optionDecision.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/optionDecision.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGS1](wireframe-gallery.html#WGS1), [WGQ2](wireframe-gallery.html#WGQ2), [WGQ3](wireframe-gallery.html#WGQ3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGQ1",
  "contextRef": "sample:dialogue",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WGS1",
      "modelRef": "model:WGS1"
    },
    {
      "componentId": "WGQ2",
      "modelRef": "model:WGQ2"
    },
    {
      "componentId": "WGQ3",
      "modelRef": "model:WGQ3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGQ2: Player portrait

**Parent: WCI2.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Player →           NPC
portrait           portrait
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ2.root | config.portraits.slot width | three times the visible band | owning component slot | scene left slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Player →           NPC
portrait           portrait
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ2.root | config.portraits.slot width | three times the visible band | owning component slot | scene left slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Player →           NPC
portrait           portrait
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ2.root | config.portraits.slot width | three times the visible band | owning component slot | scene left slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Player →           NPC
portrait           portrait
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ2.root | config.portraits.slot width | three times the visible band | owning component slot | scene left slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Use the shared artwork component and player-facing context. Anchor left and baseline bottom; no portrait interaction duplicates dialogue choices.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the dialogue source boundary. Current event screen renders authored prose and domain-backed choices. Two portraits and audio-paced caption progression are proposed; sample text is illustrative.

- [src/ui/screens/event.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/quests.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/quests.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/optionDecision.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/optionDecision.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI2](wireframe-gallery.html#WCI2)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGQ2",
  "contextRef": "sample:dialogue",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCI2",
      "modelRef": "model:WCI2"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGQ3: NPC portrait

**Parent: WCI2.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Player         ← NPC speaking
portrait         portrait
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ3.root | config.portraits.slot width | three times the visible band | owning component slot | scene right slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Player         ← NPC speaking
portrait         portrait
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ3.root | config.portraits.slot width | three times the visible band | owning component slot | scene right slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Player         ← NPC speaking
portrait         portrait
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ3.root | config.portraits.slot width | three times the visible band | owning component slot | scene right slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Player         ← NPC speaking
portrait         portrait
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ3.root | config.portraits.slot width | three times the visible band | owning component slot | scene right slot | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Use the shared artwork component with NPC asset and speaking state. Anchor right; mirror artwork only when authored facing requires it.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the dialogue source boundary. Current event screen renders authored prose and domain-backed choices. Two portraits and audio-paced caption progression are proposed; sample text is illustrative.

- [src/ui/screens/event.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/quests.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/quests.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/optionDecision.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/optionDecision.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI2](wireframe-gallery.html#WCI2)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGQ3",
  "contextRef": "sample:dialogue",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCI2",
      "modelRef": "model:WCI2"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGQ4: Caption/choice region

**Parent: WCF4.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Quest title
The forge is still warm.
[Ask about the forge] [Ask what the ash took]
[Offer your blade]    [Leave the forge]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ4.root | 100% | config.context | owning component slot | reveal line | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Quest title
The forge is still warm.
[Ask about the forge] [Ask what the ash took]
[Offer your blade]    [Leave the forge]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ4.root | 100% | config.context | owning component slot | reveal line | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Quest title
The forge is still warm.
[Ask about the forge] [Ask what the ash took]
[Offer your blade]    [Leave the forge]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ4.root | 100% | config.context | owning component slot | reveal line | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Quest title
The forge is still warm.
[Ask about the forge] [Ask what the ash took]
[Offer your blade]    [Leave the forge]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ4.root | 100% | config.context | owning component slot | reveal line | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Project one short authored beat and only known eligible choices: quest title, narrative, responses; no eyebrow, speaker line or prompt hint (the speaker is named under the portrait). Up to four responses fit without scrolling (config.W4c.context.maxVisibleResponses) in a grid whose columns come from config.W4c.context.responseColumns. Choice activation records selection; explicit Continue commits through domain.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the dialogue source boundary. Current event screen renders authored prose and domain-backed choices. Two portraits and audio-paced caption progression are proposed; sample text is illustrative.

- [src/ui/screens/event.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/quests.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/quests.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/optionDecision.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/optionDecision.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGQ4",
  "contextRef": "sample:dialogue",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB3",
      "modelRef": "model:WCB3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGQ5: Speech progression

**Parent: WCF1.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Idle → Playing → Ended → Next eligible beat
                 ↘ Await choice
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ5.root | none | none | owning component slot | nonvisual controller | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Idle → Playing → Ended → Next eligible beat
                 ↘ Await choice
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ5.root | none | none | owning component slot | nonvisual controller | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Idle → Playing → Ended → Next eligible beat
                 ↘ Await choice
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ5.root | none | none | owning component slot | nonvisual controller | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Idle → Playing → Ended → Next eligible beat
                 ↘ Await choice
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ5.root | none | none | owning component slot | nonvisual controller | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Subscribe to authored audio ended event with current generation token. Advance only linear eligible beats when config.dialogue.autoAdvance is enabled. Cancel stale events on skip/back/dispose; never invent audio timing.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the dialogue source boundary. Current event screen renders authored prose and domain-backed choices. Two portraits and audio-paced caption progression are proposed; sample text is illustrative.

- [src/ui/screens/event.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/quests.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/quests.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/optionDecision.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/optionDecision.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGQ5",
  "contextRef": "sample:dialogue",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGQ6: Back button

**Parent: WCB4.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[Back]         [Skip speech]     [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ6.root | action width | target token | owning component slot | footer left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[Back]         [Skip speech]     [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ6.root | action width | target token | owning component slot | footer left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Back]         [Skip speech]     [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ6.root | action width | target token | owning component slot | footer left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Back]         [Skip speech]     [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ6.root | action width | target token | owning component slot | footer left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Move to previous permitted caption without replaying domain effects. Disable when history does not allow navigation. Cancel current media generation.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the dialogue source boundary. Current event screen renders authored prose and domain-backed choices. Two portraits and audio-paced caption progression are proposed; sample text is illustrative.

- [src/ui/screens/event.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/quests.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/quests.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/optionDecision.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/optionDecision.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB4](wireframe-gallery.html#WCB4)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGQ6",
  "contextRef": "sample:dialogue",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB4",
      "modelRef": "model:WCB4"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGQ7: Skip speech button

**Parent: WCB3.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[Back]         [Skip speech]     [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ7.root | action width | target token | owning component slot | footer middle | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[Back]         [Skip speech]     [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ7.root | action width | target token | owning component slot | footer middle | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Back]         [Skip speech]     [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ7.root | action width | target token | owning component slot | footer middle | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Back]         [Skip speech]     [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ7.root | action width | target token | owning component slot | footer middle | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Stop current media and reveal caption. Do not skip quest content or choose a response. Ignore stale audio ended events.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the dialogue source boundary. Current event screen renders authored prose and domain-backed choices. Two portraits and audio-paced caption progression are proposed; sample text is illustrative.

- [src/ui/screens/event.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/quests.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/quests.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/optionDecision.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/optionDecision.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGQ7",
  "contextRef": "sample:dialogue",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB3",
      "modelRef": "model:WCB3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGQ8: Continue button

**Parent: WCB3.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
[Back]         [Skip speech]     [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ8.root | action width | target token | owning component slot | footer right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
[Back]         [Skip speech]     [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ8.root | action width | target token | owning component slot | footer right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Back]         [Skip speech]     [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ8.root | action width | target token | owning component slot | footer right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
[Back]         [Skip speech]     [Continue]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGQ8.root | action width | target token | owning component slot | footer right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Advance once to next permitted beat or commit explicitly selected choice. Honor readiness; cancel prior media generation and reject duplicate activation.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the dialogue source boundary. Current event screen renders authored prose and domain-backed choices. Two portraits and audio-paced caption progression are proposed; sample text is illustrative.

- [src/ui/screens/event.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/event.js#L1) — Current owner checkout (including inspected local edits)
- [src/model/quests.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/model/quests.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/components/optionDecision.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/optionDecision.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGQ8",
  "contextRef": "sample:dialogue",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB3",
      "modelRef": "model:WCB3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGS0: Shared scene layers

**Parent: none.** Use cases: Shared scene layers reusable component family. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Shared HUD
┌ Skyline ─────────────────┐
│ world scene              │
├ Floor ───────────────────┤
│ baseline / actors        │
└──────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Shared HUD
┌ Skyline ─────────────────┐
│ world scene              │
├ Floor ───────────────────┤
│ baseline / actors        │
└──────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Shared HUD
┌ Skyline ─────────────────┐
│ world scene              │
├ Floor ───────────────────┤
│ baseline / actors        │
└──────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Shared HUD
┌ Skyline ─────────────────┐
│ world scene              │
├ Floor ───────────────────┤
│ baseline / actors        │
└──────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS0.root | configured by child | configured by child | owning component slot | owning context | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Compose HUD once and background layers below. Apply visibility flags independently; shared domain facts remain owned by the HUD model.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the background source boundary. The current working game uses backdropClass in combat.js; the documentation dev baseline adds environmentArt with a cropped SVG painting. Independent skyline and floor layers are a proposed documentation contract.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/uiContent.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/uiContent.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGS1](wireframe-gallery.html#WGS1), [WGS2](wireframe-gallery.html#WGS2)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGS0",
  "contextRef": "sample:background",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WGS1",
      "modelRef": "model:WGS1"
    },
    {
      "componentId": "WGS2",
      "modelRef": "model:WGS2"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGS1: Background composition

**Parent: WCI2.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
┌ Skyline · behind ────────┐
│ distant sky and scenery  │
│                         │
├ Floor · bottom ──────────┤
│ ground under actors      │
└──────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS1.root | 100% of its host | host height | owning component slot | scene background | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
┌ Skyline · behind ────────┐
│ distant sky and scenery  │
│                         │
├ Floor · bottom ──────────┤
│ ground under actors      │
└──────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS1.root | 100% of its host | host height | owning component slot | scene background | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌ Skyline · behind ────────┐
│ distant sky and scenery  │
│                         │
├ Floor · bottom ──────────┤
│ ground under actors      │
└──────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS1.root | 100% of its host | host height | owning component slot | scene background | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌ Skyline · behind ────────┐
│ distant sky and scenery  │
│                         │
├ Floor · bottom ──────────┤
│ ground under actors      │
└──────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS1.root | 100% of its host | host height | owning component slot | scene background | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Resolve skyline and floor asset providers. Paint only enabled layers; floor height follows config.scene.floorHeightPercent and anchors bottom. Preserve shared actor baseline.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the background source boundary. The current working game uses backdropClass in combat.js; the documentation dev baseline adds environmentArt with a cropped SVG painting. Independent skyline and floor layers are a proposed documentation contract.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/uiContent.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/uiContent.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGS6](wireframe-gallery.html#WGS6), [WGS7](wireframe-gallery.html#WGS7)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGS1",
  "contextRef": "sample:background",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WGS6",
      "modelRef": "model:WGS6"
    },
    {
      "componentId": "WGS7",
      "modelRef": "model:WGS7"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGS2: Shared HUD

**Parent: WCF2.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Class          Cinders          Act / Floor
Resource meters               Armoury Menu
Relic rail
Blue experience strip when configured
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS2.root | 100% | config.hud | owning component slot | top band | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Class          Cinders          Act / Floor
Resource meters               Armoury Menu
Relic rail
Blue experience strip when configured
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS2.root | 100% | config.hud | owning component slot | top band | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Class          Cinders          Act / Floor
Resource meters               Armoury Menu
Relic rail
Blue experience strip when configured
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS2.root | 100% | config.hud | owning component slot | top band | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Class          Cinders          Act / Floor
Resource meters               Armoury Menu
Relic rail
Blue experience strip when configured
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS2.root | 100% | config.hud | owning component slot | top band | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Render the shared RunHud view model through WGH4. Toggle configured children before layout; never maintain separate combat and map HUD facts. WGH5 experience fills the full HUD host width directly below the HUD and defaults to combat context. Potions are exclusively WGC11 footer contents through WGH8; no potion or charge-flask button appears in the top HUD in either preview preset.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the hud source boundary. Source HUD establishes class/Cinders/Act/Floor, resource meters, Armoury/Menu, and inventory providers. The owner-requested reference supersedes source potion placement: top HUD contains relics only, while WGC11 opens WGH8 combined charge-flask and carried-potion contents exclusively in the footer in both presets. The proposed configurable WGH5 blue experience strip spans the full host width below the HUD, combat-only by default.

- [src/ui/components/hudmeta.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hudmeta.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/viewModels/RunHudViewModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/viewModels/RunHudViewModel.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGH4](wireframe-gallery.html#WGH4)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGS2",
  "contextRef": "sample:hud",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WGH4",
      "modelRef": "model:WGH4"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGS3: HUD identity

**Parent: WCI1.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Class            Cinders            Act / Floor
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS3.root | content-fit | HUD inner height | owning component slot | HUD left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Class            Cinders            Act / Floor
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS3.root | content-fit | HUD inner height | owning component slot | HUD left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Class            Cinders            Act / Floor
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS3.root | content-fit | HUD inner height | owning component slot | HUD left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Class            Cinders            Act / Floor
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS3.root | content-fit | HUD inner height | owning component slot | HUD left | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Project class identity and run metadata from RunHeaderModel using the same WGH7 header contract. This identity row contains only its declared fields; the separately configured WGH5 experience strip belongs below the total HUD.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the hud source boundary. Source HUD establishes class/Cinders/Act/Floor, resource meters, Armoury/Menu, and inventory providers. The owner-requested reference supersedes source potion placement: top HUD contains relics only, while WGC11 opens WGH8 combined charge-flask and carried-potion contents exclusively in the footer in both presets. The proposed configurable WGH5 blue experience strip spans the full host width below the HUD, combat-only by default.

- [src/ui/components/hudmeta.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hudmeta.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/viewModels/RunHudViewModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/viewModels/RunHudViewModel.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCI1](wireframe-gallery.html#WCI1)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGS3",
  "contextRef": "sample:hud",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCI1",
      "modelRef": "model:WCI1"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGS4: HUD resource strip

**Parent: WCM2.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
HP 32/40  [████████░░]
Mana 6/10 [██████░░░░]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS4.root | remaining | HUD inner height | owning component slot | HUD center | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
HP 32/40  [████████░░]
Mana 6/10 [██████░░░░]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS4.root | remaining | HUD inner height | owning component slot | HUD center | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
HP 32/40  [████████░░]
Mana 6/10 [██████░░░░]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS4.root | remaining | HUD inner height | owning component slot | HUD center | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
HP 32/40  [████████░░]
Mana 6/10 [██████░░░░]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS4.root | remaining | HUD inner height | owning component slot | HUD center | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Read active resource plan and semantic order from resource providers. Reuse resourceBars main surface and shared meter primitive.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the hud source boundary. Source HUD establishes class/Cinders/Act/Floor, resource meters, Armoury/Menu, and inventory providers. The owner-requested reference supersedes source potion placement: top HUD contains relics only, while WGC11 opens WGH8 combined charge-flask and carried-potion contents exclusively in the footer in both presets. The proposed configurable WGH5 blue experience strip spans the full host width below the HUD, combat-only by default.

- [src/ui/components/hudmeta.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hudmeta.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/viewModels/RunHudViewModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/viewModels/RunHudViewModel.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCM1](wireframe-gallery.html#WCM1), [WCM2](wireframe-gallery.html#WCM2)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGS4",
  "contextRef": "sample:hud",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCM1",
      "modelRef": "model:WCM1"
    },
    {
      "componentId": "WCM2",
      "modelRef": "model:WCM2"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGS5: HUD menu button

**Parent: WCB3.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
HUD right                 [Menu]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS5.root | target token | target token | owning component slot | HUD right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
HUD right                 [Menu]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS5.root | target token | target token | owning component slot | HUD right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
HUD right                 [Menu]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS5.root | target token | target token | owning component slot | HUD right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
HUD right                 [Menu]
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS5.root | target token | target token | owning component slot | HUD right | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Compose the shared Menu action only, equivalent to the WGH3 menu slot; WGH2 owns the separate Armoury action. Opening a menu follows configured simulation/input policy and restores trigger focus on dismissal.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the hud source boundary. Source HUD establishes class/Cinders/Act/Floor, resource meters, Armoury/Menu, and inventory providers. The owner-requested reference supersedes source potion placement: top HUD contains relics only, while WGC11 opens WGH8 combined charge-flask and carried-potion contents exclusively in the footer in both presets. The proposed configurable WGH5 blue experience strip spans the full host width below the HUD, combat-only by default.

- [src/ui/components/hudmeta.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/hudmeta.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/viewModels/RunHudViewModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/viewModels/RunHudViewModel.js#L1) — Current owner checkout (including inspected local edits)
- [styles/ui.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/ui.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WCB3](wireframe-gallery.html#WCB3)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGS5",
  "contextRef": "sample:hud",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WCB3",
      "modelRef": "model:WCB3"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGS6: Skyline layer

**Parent: WGS1.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
┌──────────────────────────┐
│ sky · distant silhouettes│
│       /    /          │
│      /  __/           │
└──────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS6.root | 100% scene | 100% scene | owning component slot | frame top / behind floor | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
┌──────────────────────────┐
│ sky · distant silhouettes│
│       /    /          │
│      /  __/           │
└──────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS6.root | 100% scene | 100% scene | owning component slot | frame top / behind floor | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌──────────────────────────┐
│ sky · distant silhouettes│
│       /    /          │
│      /  __/           │
└──────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS6.root | 100% scene | 100% scene | owning component slot | frame top / behind floor | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
┌──────────────────────────┐
│ sky · distant silhouettes│
│       /    /          │
│      /  __/           │
└──────────────────────────┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS6.root | 100% scene | 100% scene | owning component slot | frame top / behind floor | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Paint configured distant-art asset across scene bounds with intrinsic crop policy. Layer behind floor and actors. Omit when config.scene.skyline is disabled.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the background source boundary. The current working game uses backdropClass in combat.js; the documentation dev baseline adds environmentArt with a cropped SVG painting. Independent skyline and floor layers are a proposed documentation contract.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/uiContent.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/uiContent.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGS6",
  "contextRef": "sample:background",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGS7: Floor layer

**Parent: WGS1.** Use cases: Combat/map/dialogue composition. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
                 scene top
┌──────────────────────────┐
│ floor at bottom          │
└──────── actor baseline ──┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS7.root | 100% of host | floor line to host bottom (config.background.floorHeightPercent of the scene window) | owning component slot | floor line / ahead of skyline | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
                 scene top
┌──────────────────────────┐
│ floor at bottom          │
└──────── actor baseline ──┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS7.root | 100% of host | floor line to host bottom (config.background.floorHeightPercent of the scene window) | owning component slot | floor line / ahead of skyline | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
                 scene top
┌──────────────────────────┐
│ floor at bottom          │
└──────── actor baseline ──┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS7.root | 100% of host | floor line to host bottom (config.background.floorHeightPercent of the scene window) | owning component slot | floor line / ahead of skyline | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
                 scene top
┌──────────────────────────┐
│ floor at bottom          │
└──────── actor baseline ──┘
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS7.root | 100% of host | floor line to host bottom (config.background.floorHeightPercent of the scene window) | owning component slot | floor line / ahead of skyline | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Paint a foreground ground cutout over the full-bounds skyline using config.scene.groundCutout and config.scene.floorHeightPercent. Skyline remains underneath, visible through transparent cutout areas. Omit when config.scene.floor is disabled without shifting the actor placement model.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the background source boundary. The current working game uses backdropClass in combat.js; the documentation dev baseline adds environmentArt with a cropped SVG painting. Independent skyline and floor layers are a proposed documentation contract.

- [src/ui/screens/combat.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/screens/combat.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/uiContent.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/uiContent.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** [WGS8](wireframe-gallery.html#WGS8)

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGS7",
  "contextRef": "sample:background",
  "active": true,
  "selected": false,
  "children": [
    {
      "componentId": "WGS8",
      "modelRef": "model:WGS8"
    }
  ],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
## Wireframe WGS8: Ground formation grid

**Parent: WGS7.** Use cases: Floor-owned actor slots. Owner selection propagates to the component; no duplicated selected state.

**Wide**

```text
Allies: 1 back | 2 front         Enemies: 3 front | 4 back
A upper   [ A1 ] [ A2 ]       [ A3 ] [ A4 ]
B middle    [ B1 ] [ B2 ]   [ B3 ] [ B4 ]
C lower       [ C1 ] [ C2 ] [ C3 ] [ C4 ]
                 ┴      ┴      ┴      ┴
         shadow centers anchor to ground; equal vertical gaps
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS8.root | full floor width minus configured padding | ground contact row | owning component slot | ground contact row | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Compact**

```text
Allies: 1 back | 2 front         Enemies: 3 front | 4 back
A upper   [ A1 ] [ A2 ]       [ A3 ] [ A4 ]
B middle    [ B1 ] [ B2 ]   [ B3 ] [ B4 ]
C lower       [ C1 ] [ C2 ] [ C3 ] [ C4 ]
                 ┴      ┴      ┴      ┴
         shadow centers anchor to ground; equal vertical gaps
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS8.root | full floor width minus configured padding | ground contact row | owning component slot | ground contact row | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Portrait / iPhone SE (3rd generation)**

Reference viewport: 375 × 667 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Allies: 1 back | 2 front         Enemies: 3 front | 4 back
A upper   [ A1 ] [ A2 ]       [ A3 ] [ A4 ]
B middle    [ B1 ] [ B2 ]   [ B3 ] [ B4 ]
C lower       [ C1 ] [ C2 ] [ C3 ] [ C4 ]
                 ┴      ┴      ┴      ┴
         shadow centers anchor to ground; equal vertical gaps
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS8.root | full floor width minus configured padding | ground contact row | owning component slot | ground contact row | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |


**Portrait / Galaxy S24**

Reference viewport: 360 × 780 CSS px. Same inherited portrait layout; dimensions resolve against this viewport. Browser chrome and text scaling require separate device validation.


```text
Allies: 1 back | 2 front         Enemies: 3 front | 4 back
A upper   [ A1 ] [ A2 ]       [ A3 ] [ A4 ]
B middle    [ B1 ] [ B2 ]   [ B3 ] [ B4 ]
C lower       [ C1 ] [ C2 ] [ C3 ] [ C4 ]
                 ┴      ┴      ┴      ┴
         shadow centers anchor to ground; equal vertical gaps
```

| Component | Width | Height | Relative to | Anchor | Alignment | Positioning | Offset | Rule |
|---|---|---|---|---|---|---|---|---|
| WGS8.root | full floor width minus configured padding | ground contact row | owning component slot | ground contact row | inherited semantic alignment | normal flow unless overlay | shared token | Preserve proportions and readable minimums in every mode |

**Language-agnostic pseudocode**

```text
// Load and validate pseudocode-config.json once; inject config into this component.
// Config entries carry units; convert through the shared layout adapter.
// Wireframe IDs are identifiers. Domain facts come from the model, not config.
INPUT: immutable component model, owner state, context, layout tokens
INPUT: snapshot, knowledge, ownerState, context, config
// Load shared tokens; numeric defaults live in componentCompletionDefaults.
model = ProjectRegisteredModel(snapshot, knowledge, context)
// Compose two equal ground regions with config.groundGrid fixed padding, slot gaps and center gap. Reserve six slots per side in two columns and three staggered rows, even when empty. Fill row-major from left to right. Name each cell by row letter then battlefield column: rows A/B/C run upper to lower; columns config.referenceTokens.value_1.value to config.referenceTokens.value_4.value run left to right as ally back, ally front, enemy front, enemy back. The ally lower back cell is C1; the enemy upper back cell is A4. Resolve first/last foot baselines from available scene height and detail reserve; divide their interval into two equal steps. Assign stable ordered allies and enemies to slots from the left. Anchor WCO5 centers to slot centers. Clamp counts to slot capacity; preserve empty slots without recentering. Cache unselected sprite envelopes per category and compute a shared base fit across both factions. Selection-only details never enter the fit. Apply config.groundGrid.actorScale multiplied by configured row base and selected growth through rowPresentationScale; preserve the sprite-foot ground anchor. Lower upper/middle depth anchors by config.groundGrid.depthLoweringFractions of a row step, leaving the lower anchor fixed. Defaults [config.referenceTokens.value_0_25.value,config.referenceTokens.value_0_125.value,config.referenceTokens.value_0.value] produce equal displayed gaps of config.referenceTokens.value_0_875.value row steps. Fit scale against unadjusted baselines so these position offsets cannot enlarge sprites. Retreat inner columns toward their own side by config.groundGrid.frontRowRetreatPercent of field width, capped at config.referenceTokens.value_15Percent.value of column spacing. Tag screen-outer columns back-row and inner columns front-row independently of upper/middle/lower depth. Add config.groundGrid.formationLayers to selection paint priority so back-row guards and intents remain above front-row actors. Interpolate the team gap with config.groundGrid.centerGap. Refit only when host allocation, roster or unselected geometry changes.
FilterInactiveProviders(model)
children = ResolveDeclaredChildReferences(model.children)
RenderRegisteredComponent(model, children, config)
// Local preview actions never mutate the game. Production host revalidates commands.
On activation: DispatchSemanticIntent(model.intent, context)
On disposal: ReleaseTimersObserversAndSubscriptions()
On model change: reproject registered values; preserve stable identity
On dispose: release timers, observers and events
```


## Configuration defaults

```json
{
  "combatant": {
    "maxStackRows": 5,
    "extraBarHeightRatio": 0.5,
    "stackGapRem": 0.2,
    "iconSizeRem": 1.575,
    "defenseOffsetRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntentVisible": false,
    "enemyIntentVisible": true,
    "legibility": {
      "valueFontMinPx": 12,
      "infoSizePx": 44,
      "infoFontPx": 16,
      "hpMinRem": 0.85,
      "secondaryMinRem": 0.45,
      "stanceMinRem": 0.85,
      "iconMinRem": 1,
      "compactHPFormat": "current/maximum",
      "inspectAnchor": "original-sprite-center"
    }
  },
  "interaction": {
    "inspectDelayMs": 1000,
    "tooltipDelayMs": 1000
  },
  "card": {
    "bandFractions": {
      "header": 0.1,
      "art": 0.4,
      "body": 0.4,
      "footer": 0.1
    }
  },
  "map": {
    "regionFractions": {
      "topHUD": 0.1,
      "map": 0.6,
      "details": 0.2,
      "footer": 0.1
    },
    "inlineFraction": 0.95
  },
  "combat": {
    "regionFractions": {
      "topHUD": 0.1,
      "battlefield": 0.55,
      "hand": 0.3,
      "footer": 0.05
    },
    "hand": {
      "minimumHeightPx": 208,
      "verticalInsetRem": 0.25,
      "minFontRem": 0.7,
      "minCapacity": 5,
      "maxCapacity": 15,
      "narrowWidthRem": 22,
      "wideWidthRem": 75,
      "minCardWidthRem": 5,
      "maxCardWidthRem": 9,
      "minExposedTargetPx": 44,
      "selectedLiftRem": 0.5,
      "cardAspectRatio": "5 / 8",
      "verticalAnchor": "card-body-center"
    },
    "footer": {
      "minimumHeightPx": 56,
      "heightFraction": 0.95
    }
  },
  "inspector": {
    "widePreviewFraction": 0.34,
    "portraitPreviewFraction": 0.32,
    "gapVw": 2,
    "maxPreviewWidthRem": 14
  },
  "referenceTokens": {
    "value_1000": {
      "value": 1000,
      "unit": "count"
    },
    "value_0": {
      "value": 0,
      "unit": "count"
    },
    "value_1": {
      "value": 1,
      "unit": "count"
    },
    "value_12Percent": {
      "value": 12,
      "unit": "%"
    },
    "value_88Percent": {
      "value": 88,
      "unit": "%"
    },
    "value_15Percent": {
      "value": 15,
      "unit": "%"
    },
    "value_0_25": {
      "value": 0.25,
      "unit": "count"
    },
    "value_0_125": {
      "value": 0.125,
      "unit": "count"
    },
    "value_0_875": {
      "value": 0.875,
      "unit": "count"
    },
    "value_2": {
      "value": 2,
      "unit": "count"
    },
    "value_4": {
      "value": 4,
      "unit": "count"
    }
  }
}
```

<!-- reference-metadata:start -->
**Source description and inheritance**

Reuses the stage source boundary. Current stage derives safe formation geometry from host and actor models. Reference section percentages and layer toggles are proposed, not replacements for production geometry validation.

- [src/ui/components/battlefieldStage.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/components/battlefieldStage.js#L1) — Current owner checkout (including inspected local edits)
- [src/ui/models/BattlefieldStageModel.js:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/src/ui/models/BattlefieldStageModel.js#L1) — Current owner checkout (including inspected local edits)
- [styles/combat.css:1](https://github.com/cehinds/AshenSpire/blob/6818bb060dca1161217ef553f09e76806ec72ee4/styles/combat.css#L1) — Current owner checkout (including inspected local edits)

**Referenced components:** Leaf component; no nested component.

**Actual reference code:** [component-completion-client.js](component-completion-client.js), [component-completion.mjs](component-completion.mjs), [button-widths.mjs](button-widths.mjs)

Renderer binding: `renderCompletedComponent`. The HTML “Component composition” tab executes this implementation with the model below. Production source excerpts are references, not scripts embedded into the game.

**Styles:** [component-completion.css](component-completion.css), [exit-component.css](exit-component.css), [combatant-legibility.css](combatant-legibility.css), [card-costs.css](card-costs.css)

<details><summary>Model JSON</summary>

```json
{
  "id": "WGS8",
  "contextRef": "sample:stage",
  "active": true,
  "selected": false,
  "children": [],
  "configRef": "componentCompletionDefaults",
  "dataSource": "immutable snapshot and knowledge-filtered registered providers",
  "sampleDataRef": "componentCompletionDefaults.samples"
}
```

</details>

<details><summary>Configuration defaults JSON</summary>

```json
{
  "buttonWidths": {
    "standardHeight": "2.75rem",
    "heightMultipliers": {
      "standard": 1,
      "tall": 1.5,
      "double": 2
    },
    "sizes": {
      "third-standard": {
        "width": "third",
        "height": "standard"
      },
      "third-tall": {
        "width": "third",
        "height": "tall"
      },
      "third-double": {
        "width": "third",
        "height": "double"
      },
      "half-standard": {
        "width": "half",
        "height": "standard"
      },
      "half-tall": {
        "width": "half",
        "height": "tall"
      },
      "half-double": {
        "width": "half",
        "height": "double"
      },
      "full-standard": {
        "width": "full",
        "height": "standard"
      },
      "full-tall": {
        "width": "full",
        "height": "tall"
      },
      "full-double": {
        "width": "full",
        "height": "double"
      }
    },
    "presets": {
      "quarter": 25,
      "third": 30,
      "half": 50,
      "full": 100
    },
    "choice": "half",
    "minimumReadable": "8rem",
    "iconSize": "2.75rem",
    "footer": "equalSharesAfterGaps",
    "singleFooter": "full"
  },
  "combatantFocus": {
    "rowIds": [
      "upper",
      "middle",
      "lower"
    ],
    "rowBase": [
      0.9,
      0.95,
      1
    ],
    "selectedGrowth": [
      1.1,
      1.05,
      1.1
    ],
    "rowZPriority": [
      10,
      20,
      30
    ],
    "focusZ": 100,
    "fit": {
      "maximumScale": 1,
      "minimumScale": 0.01,
      "inset": 0,
      "gap": 0
    },
    "selectionDetailsAffectFit": false,
    "sharedBaseAcrossFactions": true,
    "stableAnchor": "sprite-baseline-center"
  },
  "spacing": {
    "gapRem": 0.2,
    "insetRem": 0.75,
    "sectionGapRem": 0.75
  },
  "target": {
    "minRem": 2.75,
    "iconRem": 1.575,
    "largeRem": 3.4,
    "smallRem": 2.75
  },
  "meter": {
    "healthHeightRem": 1.4,
    "secondaryHeightRatio": 0.5,
    "stanceHeightRatio": 1,
    "maxRows": 5
  },
  "selection": {
    "revealDelayMs": 1000,
    "tooltipDelayMs": 1000,
    "liftRem": 0.25
  },
  "overlay": {
    "defenseAnchorRatio": 0.12,
    "defenseAnchorByRole": {
      "player": 0.12,
      "enemy": 0.88
    },
    "defenseGapRem": 0.5,
    "playerIntent": false,
    "enemyIntent": true
  },
  "combatantVisibility": {
    "unselectedHiddenSelectors": [
      ".combatant-nameplate",
      ".combatant-meter:not(:first-child)",
      ".buildup",
      ".stance"
    ]
  },
  "groundGrid": {
    "displayScale": 1.1,
    "rowScaleFactors": [
      0.9,
      0.95,
      1
    ],
    "preserveInformationSize": true,
    "slotsPerSide": 6,
    "columns": 2,
    "rows": 3,
    "depthPercent": 58,
    "rowStepRem": 4.5,
    "paddingRem": 1,
    "slotGapRem": 1.5,
    "centerGapRem": 3,
    "centerGapPercent": 3,
    "centerGap": {
      "narrowPercent": 3,
      "widePercent": 5,
      "narrowWidthPx": 375,
      "wideWidthPx": 1200
    },
    "formationLayers": {
      "frontRow": 0,
      "backRow": 200
    },
    "backRowOffsetPercent": 5,
    "minimumRowOffsetPx": 8,
    "depthLoweringFractions": [
      0.25,
      0.125,
      0
    ],
    "frontRowRetreatPercent": 2,
    "actorScale": 1,
    "showGuides": true
  },
  "groundShadow": {
    "enabled": true,
    "widthPercent": 55,
    "heightPercent": 7,
    "opacity": 0.4,
    "color": "#000000",
    "anchorXFraction": 0.5
  },
  "scene": {
    "detailEdgeInsetPx": 4,
    "baselineInsetPx": 4,
    "minimumSpriteHeightPx": 92,
    "selectionDetailReserveRem": 3.5,
    "groundContactDepthFraction": 0.3,
    "actorBaselinePercent": 66.667,
    "actorHeightFraction": 0.72,
    "actorSlotPadding": 1,
    "actorWidthRem": 12,
    "groundCutout": "polygon(0 12%, 18% 8%, 36% 14%, 55% 5%, 78% 10%, 100% 3%, 100% 100%, 0 100%)",
    "floorHeightPercent": 80,
    "playerWidthPercent": 25,
    "enemyWidthPercent": 65,
    "playerCount": 1,
    "enemyCount": 2,
    "skyline": true,
    "floor": true
  },
  "combatantLegibility": {
    "intentRowMinPx": 22,
    "valueFontMinPx": 13.2,
    "infoVisualScale": 0.64,
    "infoSizePx": 44,
    "infoFontPx": 16,
    "barMinWidthPx": 0,
    "fontMinRem": 0.75,
    "hpMinRem": 0.85,
    "secondaryMinRem": 0.45,
    "stanceMinRem": 0.85,
    "iconMinRem": 1
  },
  "footerLayout": {
    "circleMaxFraction": 0.2,
    "pileMaxFraction": 0.1,
    "endMaxFraction": 0.4,
    "gapRem": 0.2,
    "heightFraction": 0.95,
    "minimumHeightPx": 56
  },
  "hand": {
    "infoVisualSizePx": 28,
    "infoHitSizePx": 44,
    "infoGapPx": 4,
    "reorderThresholdPx": 8,
    "edgeScrollPx": 24,
    "edgeScrollStepPx": 12,
    "horizontalInsetRem": 0.625,
    "inspectRiseRem": 0.5,
    "fanMaxDegrees": 2.5,
    "fanArchPx": 8,
    "bodyUpPx": 0,
    "minimumHeightPx": 208,
    "verticalInsetRem": 0.25,
    "minFontRem": 0.7,
    "minCapacity": 5,
    "maxCapacity": 15,
    "narrowWidthRem": 22,
    "wideWidthRem": 75,
    "minCardWidthRem": 5,
    "maxCardWidthRem": 9,
    "minExposedTargetPx": 44,
    "selectedLiftRem": 1,
    "cardWidthRem": 7,
    "cardAspectRatio": "5 / 8",
    "gapRem": 0.2,
    "fixtureIds": [
      "WC1a",
      "WC1b",
      "WC1c",
      "WC1a",
      "WC1b"
    ]
  },
  "tooltip": {
    "widthRem": 18,
    "maxWidthPercent": 95
  },
  "map": {
    "viewBox": "0 0 100 100",
    "selectedNode": "town",
    "nodeSizeRem": 2.75
  },
  "dialogue": {
    "speakingId": "keeper",
    "autoAdvance": false,
    "currentBeat": "welcome"
  },
  "samples": {
    "health": {
      "label": "HP",
      "current": 32,
      "maximum": 40
    },
    "resource": {
      "label": "Mana",
      "current": 6,
      "maximum": 10
    },
    "buildup": {
      "label": "Burn buildup",
      "current": 65,
      "maximum": 100
    },
    "stance": {
      "label": "Aggressive",
      "active": true
    },
    "intent": {
      "label": "Attack",
      "icon": "⚔",
      "amount": 12,
      "active": true
    },
    "defense": {
      "label": "Block",
      "icon": "◇",
      "amount": 8,
      "active": true
    },
    "footer": {
      "actions": 3,
      "draw": 12,
      "discard": 4,
      "exhaust": 1,
      "potions": 2
    },
    "statuses": [
      {
        "id": "regen",
        "icon": "✚",
        "name": "Regeneration",
        "detail": "2 stacks · 3 turns"
      },
      {
        "id": "poison",
        "icon": "☠",
        "name": "Poison",
        "detail": "3 stacks · 2 turns"
      },
      {
        "id": "weak",
        "icon": "↓",
        "name": "Weakened",
        "detail": "1 stack · 1 turn"
      }
    ],
    "mapNodes": [
      {
        "id": "start",
        "label": "Visited",
        "icon": "●",
        "x": 50,
        "y": 82,
        "state": "visited"
      },
      {
        "id": "combat",
        "label": "Combat",
        "icon": "⚔",
        "x": 25,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "town",
        "label": "Town",
        "icon": "⌂",
        "x": 73,
        "y": 53,
        "state": "reachable"
      },
      {
        "id": "unknown",
        "label": "Unknown",
        "icon": "?",
        "x": 50,
        "y": 18,
        "state": "blocked"
      }
    ],
    "mapEdges": [
      [
        "start",
        "combat"
      ],
      [
        "start",
        "town"
      ],
      [
        "combat",
        "unknown"
      ],
      [
        "town",
        "unknown"
      ]
    ],
    "beats": [
      {
        "id": "welcome",
        "speaker": "The Keeper",
        "text": "The forge is still warm. What brings you here?"
      },
      {
        "id": "offer",
        "speaker": "The Keeper",
        "text": "I can mend what the ash has taken.",
        "choices": [
          "Ask about the forge",
          "Ask what the ash took",
          "Offer your blade for mending",
          "Leave the forge"
        ]
      }
    ]
  }
}
```

</details>
<!-- reference-metadata:end -->
