# Creating a layout

A plugin cannot invent panels at runtime. It can toggle CSS classes and set dialog-variable strings on panels that **already exist** on the client. A custom component is therefore a fixed XML slot plus the classes and `{s:…}` variables the server will drive.

[Mounting your own](https://s2script.com/docs/concepts/hud/custom) binds that markup with a `CustomHudSpec`. This page is how to write the markup.

## Files

```
panorama/layout/custom_game/<name>.xml     ← source .xml, never .vxml / .vxml_c
panorama/styles/custom_game/<name>.css     ← source .css, never .vcss
```

The include line is load-bearing. Wrong scheme or extension compiles clean and the stylesheet never attaches:

```xml
<include src="file://{resources}/styles/custom_game/killfeed.css" />
```

Not `s2r://`, no `panorama/` prefix, and the **source** `.css`. A resolved include makes the compiler print the `.css` a second time — that is the tell.

`resourcecompiler` will not stop you from using illegal attributes. The client is the only CSS validator: it pops a dialog naming file, line, column, property, and value. Load once after every CSS change.

## What you can put in the XML

`custom_hud_layout` exposes a closed set. From Valve's `point_script.d.ts`:

| Element    | Attributes                       |
| ---------- | -------------------------------- |
| `<Panel>`  | `id`, `class`, `hittest`         |
| `<Label>`  | `id`, `class`, `hittest`, `text` |
| `<Image>`  | `id`, `class`, `hittest`, `src`  |
| `<Button>` | `id`, `class`                    |

No `onactivate`, no `style=`, no client-side script. Events are ignored with no error. Every offset and colour lives in a class.

The root `<Panel>` **must not** have an `id` — hard compile error. Put a theme class on the first child instead.

A panel does not render for a player until that player has per-player state. Do **not** put `hideClass` in the markup: removing a markup-declared class is not the same as clearing a per-player one. Declare the hide class in CSS; the server applies it with `hide()`.

## Dialog variables

Text is `{s:name}` on a `<Label>`. The first argument of the engine setter is a panel id, so every label that carries a `{s:}` needs its own `id`.

Prefer **id equals variable name**. Then the plugin can `layout.forSlot(slot).set('feed_0_a', name)` with no `text` map — or `p.set({ feed_0_a: name })`.

```xml
<Label id="feed_0_a" class="kf-name" text="{s:feed_0_a}" />
```

An unset `{s:…}` renders empty. A correctly-loaded production layout looks like a blank frame until the plugin fills it. Ship a literal-text probe (`CustomHudLayout.probe()` / the shipped `s2script_hud.xml` paints **"S2SCRIPT PROBE OK"**) when you want to eyeball mount with no plugin attached.

Never generate names dynamically (`row_${page}_${i}`). Interning is per name, forever, into three × 1024 vectors shared by every plugin on the entity. Reuse a fixed pool and change content.

## A kill-feed row

Two stacked rows, three labels each. The server fills them with `setPool`. CSS uses flow, not flex.

```xml
<root>
  <styles>
    <include src="file://{resources}/styles/custom_game/killfeed.css" />
  </styles>

  <Panel class="kf-root">
    <Panel id="feed_0" class="kf-row">
      <Label id="feed_0_a" class="kf-name" text="{s:feed_0_a}" />
      <Label id="feed_0_w" class="kf-weapon" text="{s:feed_0_w}" />
      <Label id="feed_0_v" class="kf-name" text="{s:feed_0_v}" />
    </Panel>
    <Panel id="feed_1" class="kf-row">
      <Label id="feed_1_a" class="kf-name" text="{s:feed_1_a}" />
      <Label id="feed_1_w" class="kf-weapon" text="{s:feed_1_w}" />
      <Label id="feed_1_v" class="kf-name" text="{s:feed_1_v}" />
    </Panel>
  </Panel>
</root>
```

```css
.kf-root {
	width: 100%;
	height: 100%;
}

.kf-row {
	horizontal-align: right;
	vertical-align: center;
	flow-children: right;
	padding: 8px 12px;
	margin-bottom: 4px;
	background-color: #0d1219cc;
}

.kf-name {
	color: #e8f0f4ff;
	font-size: 16px;
}
.kf-weapon {
	color: #d9aa45ff;
	font-size: 14px;
	margin: 0px 10px;
}

.s2-hide {
	opacity: 0;
}
```

Layout is flow-based: `flow-children: right | down`, `width` / `height` as `fit-children` or `fill-parent-flow(w)`, plus `position`, `align`, `ignore-parent-flow`. These silently do nothing and the compiler does not warn:

`display` · `flex` / `grid` and companions · `top` / `right` / `bottom` / `left` · `calc()` · `var()` · `rgb()` / `rgba()` · `@media` · `:not()` · `::before` / `::after`

Colours are `#rrggbbaa`. Widths that change at runtime are step classes (`s2-w0`…`s2-w10`) — the server cannot set a style property.

Buttons are real `<Button id="…">` elements. The `id` is the `buttonId` `HudLayout.onClick` receives. Clicks reach the server; a CSS `:disabled` or grey class does not block them — call `setDisabled`.

Hover-to-reveal is the only client-side interactivity:

```css
.kf-tip-host:hover .kf-tip {
	opacity: 1;
}
```

## Buy menu and scoreboard

CS2 puts `HUD_BUYMENU_VISIBLE` and `HUD_SCOREBOARD_VISIBLE` on an ancestor panel. Use descendant selectors — not server class writes:

```css
.HUD_SCOREBOARD_VISIBLE .my-overlay,
.HUD_BUYMENU_VISIBLE .my-overlay {
	visibility: collapse;
}
```

Custom layouts always draw **above** the base HUD; `z-index` only orders custom content. The visibility class does not expose the native buy menu's purchase controls.

The shipped library CSS hides passive toasts, badges, banners, and callouts while either base panel is open. Interactive sheets (modals, MOTD, dashboard) stay visible: CSS cannot release server-owned cursor capture. Do not hide a cursor-capturing panel with CSS alone.

**Compile and republish the workshop stylesheet** to deliver CSS changes. Updating the server bundle alone does not update clients' workshop assets.

## Bind it

The spec names every id the plugin will touch. Unused markup is free; a name is interned the first time a setter sees it. `hideClass` defaults to `"s2-hide"`; `observable` defaults to `false`; `text` / `buttons` / `meters` default empty.

```ts
const KILLFEED: CustomHudSpec = {
	addons: ['YOUR_WORKSHOP_ID'],
	resource: 'panorama/layout/custom_game/killfeed.xml',
	slots: {
		rows: [
			{ id: 'feed_0', vars: ['feed_0_a', 'feed_0_w', 'feed_0_v'] },
			{ id: 'feed_1', vars: ['feed_1_a', 'feed_1_w', 'feed_1_v'] }
		]
	}
};
```

Then `CustomHudLayout.create(KILLFEED)` and `forSlot` / `setPool` as on [Mounting your own](https://s2script.com/docs/concepts/hud/custom).

`hudkit` is the shared panel pool (toast, badge, modal, callout, banner, motd, the vote rail, and the TopMenu dashboard on `s2script_lib.xml`). A new component is a new spec plus `CustomHudLayout.create`, not a call to `hudkit.badge`. `hudkit.motd` is the shipped MOTD overlay; `hudkit.dashboard` is the shipped admin hub — dedicated roots, not pooled `s2_mN` sheets. Do not invent a second MOTD type or fold those surfaces into the modal pool on this primitive.

The shipped lib's stylesheet include must stay `file://{resources}/styles/custom_game/s2script_lib.css`. An `s2r://` include compiles clean and ships unstyled. Lib source (XML + CSS, including six `s2_m0`–`s2_m5` sheets, `s2_vote*`, and `s2_dash`) is in [`examples/hud-lab/workshop/`](https://github.com/s2script/s2script/tree/main/examples/hud-lab/workshop); republish the same workshop item (`3790153369`) after compiling on Windows Workshop Tools, **before** deploying a runtime whose modal pool is 6. Do not add a second layout.

## Traps

- **Blank ≠ failed load.** Unset `{s:…}` is empty. Use a literal-text probe to prove mount.
- **Root panel has no `id`.** Theme class goes on the first child.
- **Do not put `hideClass` in the XML.** Server applies it per player.
- **Animated hide is two-phase.** `visibility: collapse` cannot be transitioned. Prefer `Hud.showAnimated` / `hideAnimated` / `flash`, and a generation counter so a second event does not yank the first mid-fade.
- **Reveal synchronously.** Clear any `*-out` class, then clear hide, in the same call. A failed hide is obvious; a failed reveal is an invisible panel capturing input.

Deep compile traps live in the monorepo example [`examples/hud-lab/workshop/PANORAMA.md`](https://github.com/s2script/s2script/blob/main/examples/hud-lab/workshop/PANORAMA.md).

## See also

- [Mounting your own](https://s2script.com/docs/concepts/hud/custom) — spec + drive
- [Default components](https://s2script.com/docs/concepts/hud/components)
- [HUD](https://s2script.com/docs/concepts/hud)
