# Weave UI

Sindri AI knowledge pack, file `08-weave-ui.md`. Screen UI and HUDs: Weave stylesheets, responsive layout, and the full Weave language reference.

Start with `00-start-here.md` if you have not read it. Every fact here is copied from the Sindri repository; nothing in this file is a guess.

## Contents of this file

- **Weave presentation styling** (from `docs/weave.md`): Documentation map; Quick start; Stylesheet composition; Selectors; Properties; Lengths and responsive rules; Runtime flow; Editor workflow; Current limits
- **Weave language reference** (from `docs/weave-reference.md`): Project setup; Stylesheet composition; Rule syntax; Cascade; Lengths; Layout properties; Shape properties; Text properties; Responsive rules; Runtime behavior; Transitions; Pointer states in the running game; Inspecting styles in the editor; Authoring guidance; Errors and unsupported declarations; Diagnostic checklist
- **Weave properties** (from `docs/generated/weave-reference.md`): Size and position; Box; Flex layout; Grid layout; Text; Shape; Motion

---

Source: `docs/weave.md`

## Weave presentation styling

Weave is an experimental presentation language for Sindri screen UI. It lets a
project keep responsive layout and visual styling beside its scene instead of
baking every viewport choice into component JSON.

The language remains independent in the `weave/` workspace. The
`sindri-weave` bridge is the only crate that knows both Weave and Sindri. It
resolves a disposable presentation copy of an authored `World`; the source
world is never mutated.

The browser proof lives at `games/weave-poc` and is deployed to
`/sindri-engine/examples/weave-poc/`.

### Documentation map

- This page is the short conceptual guide.
- `weave-reference.md` (in `08-weave-ui.md`) is the complete implemented
  language reference, including diagnostics and unsupported CSS.
- `weave-poc.md` (`docs/weave-poc.md` in the Sindri repository) records the architectural boundary and proof
  strategy.
- `weave-migration.md` (`docs/weave-migration.md` in the Sindri repository) is the step-by-step real-game
  migration guide and acceptance checklist.
- `games/weave-poc/assets/demo.weave` is the smallest responsive example.
- `games/orbital-baked/assets/ui.weave` is the production-oriented entry
  stylesheet; it composes focused HUD, overlay, and screen styles with `@use`.

### Quick start

1. Keep UI entities, hierarchy, components, initial content, and stable IDs in
   the scene.
2. Add presentation classes with the opaque `weave.style` component.
3. Add one entry `.weave` file to the project's `[assets].include` list. Imported
   stylesheets are discovered by the exporter.
4. Split larger presentation surfaces with top-level `@use "...";` directives.
5. Style broad roles with classes and reserve ID rules for true exceptions.
6. Add portrait or width media rules that recompose the hierarchy.
7. Verify rendering and pointer hit testing at wide, narrow, and portrait sizes.

```toml
[assets]
include = ["ui/game.weave"]
```

```css
@use "shared/theme.weave";
@use "screens/menu.weave";

.panel {
    width: 560px;
    padding: 24px;
    direction: column;
    gap: 16px;
}

.action {
    width: 240px;
    height: 52px;
    border-radius: 12px;
}

@media (orientation: portrait) {
    .panel { width: 92vw; }
    .action { width: 100%; }
}
```

Weave owns presentation only. Decay should continue updating the same stable
entity IDs with `Ui.set_text`, `Ui.set_number`, `Ui.set_numbers`, and
`Ui.set_fill`.

### Stylesheet composition

`@use` is a deliberately small file-composition primitive, not a second module
system. Directives must be top-level and appear before the first selector or
media query:

```css
@use "shared/theme.weave";
@use "hud/status.weave";
```

Paths are resolved relative to the importing stylesheet's asset ID and always
use forward slashes. `.` and `..` are normalized, but an import may not escape
the stylesheet root. Imported rules are inserted before the importing file's
own rules, so ordinary Weave specificity and source order continue to determine
the winner.

The exporter follows the complete `@use` graph from an entry stylesheet in
`[assets].include`, so imported files do not also need to be listed in
`sindri.toml`. The browser host composes the fetched source graph and does not
apply imported files a second time. Missing sources, malformed directives,
root escapes, and circular imports are errors with the stylesheet path/import
chain in the diagnostic.

Independent `.weave` files may still be listed as separate roots. They are
composed independently and applied in deterministic asset order.

### Selectors

Selectors are CSS's: element names, IDs, classes, states, compounds,
descendant and child combinators, and lists.

```css
#menu { width: 620px; }
.menu-button { height: 64px; }
text { text-align: center; }
.menu > button.primary:hover { background: var(--accent); }
.menu text, .dialog text { color: #f8fafc; }
```

An element name is a UI component's short name (`text`, `image`, `shape`,
`button`, `slider`, `layout`); the full `sindri.ui.text` still works. Classes
are presentation metadata in an opaque scene component:

```json
"weave.style": {
  "classes": ["menu-button", "primary"]
}
```

The most specific rule wins, counting IDs, then classes and states, then
element names; source order breaks ties. Text properties and `--custom`
properties inherit from the parent entity, and `var(--name, fallback)`
substitutes a custom property. `weave-reference.md` (in `08-weave-ui.md`) has
the complete rules.

### Properties

The bridge translates declarations into existing Sindri data. It does not add a
second renderer or a parallel UI object model.

Sizing runs only when a width, height, or min/max sizing declaration applies.
Typography and unrelated rules preserve gameplay transforms, including absent
transforms and mirrored scales. A transformless tagged entity therefore remains
nonspatial after the demo's stylesheet settles; styling does not make it eligible
for `World.nearest` or `World.within_radius`.

| Weave property | Sindri target |
| --- | --- |
| `width`, `height` | entity transform scale |
| `min-width`, `max-width`, `min-height`, `max-height` | constraints applied to final transform scale |
| `padding` | content-box inset used when percentage-sized children resolve |
| `x`, `y` | entity transform position |
| `anchor` | UI image, shape, and text anchor |
| `direction`, `gap` | `sindri.ui.layout` |
| `justify-content` | main-axis distribution on `sindri.ui.layout` |
| `align-items` | cross-axis alignment on `sindri.ui.layout` |
| `background` | `sindri.ui.shape.fill` |
| `border-color`, `border-width`, `border-radius` | shape stroke and corner fields |
| `color` | `sindri.ui.text.color` |
| `font-size`, `line-height`, `letter-spacing` | text metrics |
| `font-weight` | text bold state |
| `text-align` | text line alignment |
| `text-wrap` | text wrapping (`none`, `word`/`wrap`, `glyph`) |
| `text-transform` | text case |

`justify-content` accepts `start`, `center`, `end`, and `space-between`.
`align-items` accepts `start`, `center`, and `end`. The engine's generic UI
layout owns the actual placement, so rendering, hit testing, and editor handles
all consume the same resolved positions rather than learning Weave-specific
rules.

Colors accept `transparent`, `black`, `white`, and three-, four-, six-, or
eight-digit sRGB hex notation; the bridge converts color channels to the renderer's
linear space while leaving alpha unchanged. Font weight accepts `normal`, `400`, `bold`, or
`700`. Text transform accepts `none`, `uppercase`, or `lowercase`.

Unknown properties are currently ignored. Invalid values for known properties
return an `ApplyError` naming the entity, property, and value.

### Lengths and responsive rules

Lengths may be unitless overlay values, pixels, `vw`, `vh`, or percentages.
Percentage width, height, and min/max constraints resolve against the parent's
final content box rather than the viewport. Uniform `padding` shrinks that
content box for descendants, so `width: 100%` fills the padded interior instead
of the parent's outer box. Percentage border width and radius remain relative to
the final styled box.

Sindri's overlay is two units tall, so the bridge resolves viewport-dependent
units before ordinary scene extraction. Parent boxes settle before descendants,
which keeps percentage sizing deterministic even when a child appears before its
parent in authored scene order.

Supported media conditions are orientation and minimum or maximum width and
height, combined with `and` and commas as in CSS:

```css
@media (orientation: portrait) { /* ... */ }
@media (max-width: 700px) { /* ... */ }
@media (min-width: 900px) and (orientation: landscape) { /* ... */ }
@media (max-height: 500px), (max-width: 400px) { /* ... */ }
```

### Runtime flow

1. Load the authored scene into a normal `World`.
2. Load the project's `.weave` source graph and compose each independent entry
   stylesheet.
3. Resolve selector matches and cascade winners into a computed style for each
   entity.
4. Resolve a `PresentationWorld` for the current viewport, settling ancestors
   before descendants so constraints, padding, and percentages have a stable
   basis.
5. Feed its ordinary transforms and component payloads into the existing
   extraction, rendering, layout, and input paths.
6. Resolve again from the authored world when the viewport changes.

This keeps Weave reversible: removing the bridge leaves the engine and scene
model intact.

### Editor workflow

The editor reads `.weave` roots from `[assets].include`, follows each root's
`@use` graph, and applies the same ordered composition as export to both Scene
and Game views. An entity's stable ID is its `#id` selector; reusable `.class`
selectors are authored in the inspector's **Weave Style** section. Selecting a
stylesheet in the project browser opens a read-only source preview.

Saved changes to the manifest or any stylesheet in the source graph are picked
up automatically. A failed reload reports the source location and preserves
the last good presentation until another file change succeeds. Source editing
and named viewport presets are not integrated into the editor yet.

### Current limits

Still smaller than browser CSS, and closing the gap is the plan in
`ui-direction.md` (`docs/ui-direction.md` in the Sindri repository). It has no `@keyframes`, borders per side,
accessibility mapping,
integrated stylesheet editor, or named viewport presets.
The demo proves reusable classes, cascade behavior, responsive geometry,
min/max constraints, uniform content padding, main/cross-axis alignment, font
metrics, fills, strokes, rounded shapes, and composable stylesheet sources on
the native Sindri UI path.

---

Source: `docs/weave-reference.md`

## Weave language reference

Weave is Sindri's responsive presentation language for screen UI. A stylesheet
selects existing scene entities and resolves ordinary Sindri UI component data
for a viewport. It does not create gameplay entities, replace Decay, or introduce
a browser document model.

This reference describes the currently implemented language. Unsupported CSS
features are listed explicitly so authors do not have to discover them by trial
and error.

### Project setup

Add one or more entry `.weave` files to the project's asset set:

```toml
[assets]
include = ["ui/game.weave"]
```

Imported stylesheets do not also need to be listed. The exporter follows the
entry file's `@use` graph and ships every referenced `.weave` source.

The scene remains the source of entity identity, hierarchy, components, initial
content, and gameplay bindings. Use `weave.style.classes` for reusable
presentation roles:

```json
"weave.style": {
  "classes": ["hud-card", "danger"]
}
```

Removing the stylesheet restores the authored presentation because Weave
resolves a disposable copy of the world.

### Stylesheet composition

A stylesheet may import focused presentation files with top-level `@use`
directives before the first rule or media query:

```css
@use "shared/theme.weave";
@use "hud/status.weave";

.screen {
    width: 100%;
}
```

Both single and double quoted paths are accepted. Import paths are resolved
relative to the importing stylesheet asset ID, use forward slashes on every
platform, and may contain `.` or `..` so long as they do not escape the
stylesheet root.

Imported rules are inserted before the importing file's own rules. Specificity
and source order therefore behave exactly as though the imported rules had been
written first in the entry stylesheet. A source imported more than once in one
composition graph is emitted once.

Multiple independent entry stylesheets remain supported. The runtime identifies
which fetched `.weave` files are roots, composes each root, and does not apply an
imported file a second time.

Composition errors include malformed `@use` directives, missing source IDs,
paths that escape the stylesheet root, and circular imports. A cycle reports the
chain, for example `ui.weave -> shared.weave -> ui.weave`.

### Rule syntax

```css
selector {
    property: value;
}
```

A declaration ends with a semicolon. A rule may contain multiple declarations.
Selectors are written as in CSS:

| Form | Example | Matches |
| --- | --- | --- |
| Element | `text`, `button`, `shape`, `slider` | An entity with that UI component |
| Entity ID | `#hud-score` | The stable scene entity ID |
| Class | `.hud-value` | An entry in `weave.style.classes` |
| Universal | `*` | Any entity |
| State | `:hover`, `:active` (or `:pressed`), `:focus`, `:disabled`, `:checked`, `:open` | An entity in that state |
| Compound | `button.primary:hover` | All of its parts at once |
| Descendant | `.menu text` | The right-hand entity anywhere inside the left |
| Child | `.menu > button` | The right-hand entity directly inside the left |
| Next sibling | `text + slider` | The right-hand entity straight after the left, under the same parent |
| Later sibling | `.primary ~ button` | The right-hand entity anywhere after the left, under the same parent |
| Position | `:first-child`, `:last-child`, `:only-child`, `:nth-child(2n+1)`, `:nth-last-child(2)`, `:root` | An entity by where it sits among its siblings, in scene order |
| Negation | `button:not(.primary, :first-child)` | An entity none of the list matches |
| Any of | `:is(#a, .b)`, `:where(#a, .b)` | An entity any of the list matches |
| List | `.title, .subtitle` | Either |

An element name is a UI component's short name: `text` for `sindri.ui.text`,
`image`, `shape`, `button`, `slider`, `layout`, and likewise `sprite` for
`sindri.sprite`. The full name still works, so `sindri.ui.text { … }` means
what it always did.

Siblings are an entity's parent's children in the scene's order, and the
scene's top-level entities for one with no parent; `:root` is an entity with
no parent. `:nth-child()` takes CSS's `an+b`, `odd` and `even`. `:not()` and
`:is()` are as specific as their most specific argument, and `:where()` is
not specific at all, as in CSS.

`:disabled` and `:checked` come from the entity's own data: a component with
`"disabled": true` makes its entity `:disabled`, and a toggle with
`"checked": true` makes it `:checked` (as does a dropdown's selected option),
and a dropdown whose list is showing is `:open`. `:hover`, `:active` and `:focus` come
from input, which a host passes in when it resolves presentation
(`PresentationWorld::resolve_with_states`). `:focus` is whichever button,
toggle, text field or scroll region has the keyboard, by Tab or by a press.

### Cascade

For each property independently, as in CSS:

1. A matching rule with greater specificity wins. Specificity counts IDs, then
   classes and states, then element names: `#play` beats
   `.menu .row button.primary:hover`, which beats `button:hover`, which beats
   `button`.
2. At equal specificity, the declaration appearing later in the stylesheet wins.
3. A declaration inside an active media rule participates in the same cascade.
4. Text properties inherit: `color`, `font-size`, `font-weight`, `font-style`,
   `line-height`, `letter-spacing`, `text-align`, `text-transform` and
   `text-wrap`. Colour a panel and every label inside it takes that colour
   unless it says otherwise. Box properties such as `width` do not inherit.
5. `inherit` takes the parent's value for any property; `initial` returns a
   property to the entity's authored value; `unset` inherits a text property
   and resets any other.

```css
.hud { color: #cbd5e1; font-size: 18px; }   /* every label in the HUD */
.hud .warning { color: #f87171; }            /* except warnings */
#hud-hp-text { color: #f8fafc; }             /* and this one */
```

#### Custom properties

A property whose name starts with `--` is a variable: it inherits like text
does, and `var(--name)` stands for its value wherever a value is written. A
fallback follows a comma. Put a theme's tokens on the outermost element and use
them anywhere inside it:

```css
.screen {
    --accent: #f97316;
    --panel: #111827;
    --radius: 12px;
}
.card { background: var(--panel); border-radius: var(--radius); }
.card:hover { border-color: var(--accent); }
.badge { background: var(--badge, var(--accent)); }
```

A `var()` naming nothing, with no fallback, makes its declaration invalid, and
the property keeps the entity's authored value, as in CSS. Variables that
refer to each other in a circle are treated the same way.

### Lengths

| Unit | Meaning |
| --- | --- |
| no unit | Sindri overlay units |
| `px` | CSS-style logical pixels converted for the viewport |
| `vw` | Percentage of viewport width |
| `vh` | Percentage of viewport height |
| `%` | Percentage of the parent's resolved content box |

Sindri's overlay is two units tall. Prefer pixels for stable desktop controls,
viewport units for deliberate screen-relative composition, and percentages for
children that should track a parent.

Percentage descendants resolve after the parent. Uniform `padding` reduces the
parent content box, so `width: 100%` fills the padded interior rather than
covering the outer border.

`min-*` and `max-*` constraints apply to the final resolved size.

`calc()` mixes units wherever a length goes: `width: calc(100% - 40px)`,
`padding: calc(var(--gap) * 2) 8px`. Lengths add and subtract, and multiply
or divide by plain numbers; `u` names the overlay's own unit inside one.
`min()`, `max()` and `clamp()` are not read yet.

### Layout properties

Every property, with the values it accepts, what it writes and an example, is listed
in the generated Weave property reference (in `08-weave-ui.md`), read from
the table Sindri applies declarations by. A property not listed there is ignored.

Layout is hierarchical. A child must be parented beneath the layout entity in
the scene; visual overlap does not establish layout membership. Explicit child
boxes are important because text metrics alone do not currently provide
intrinsic layout sizing.

Layout is CSS flexbox. Children are put in `order`, broken into lines when
the layout wraps, and each line's spare room is shared by `flex-grow` or its
shortfall taken back by `flex-shrink` (weighted by size, and one by default,
as in CSS), within `min-`/`max-` limits; then the line is justified and each
child aligned across it. Wrapped lines share the layout's leftover height, as
CSS's default `align-content` does. What the layout decides is what is drawn
and what is clicked. `flex-start` and `flex-end` are accepted for `start` and
`end`. Two differences from a browser: limits are applied in one pass rather
than by re-sharing what a clamped child could not take, and a child that is
not itself a layout has no automatic minimum, because text is not measured
yet. A layout's automatic minimum is its children and padding, so a badge
does not shrink into its own label.

A grid (`display: grid`, or a `sindri.ui.grid` component in the scene) places
its children in columns and rows. Tracks are fixed lengths, `auto` (as big as
the largest item alone in the track) or `fr` shares of the room those leave;
an `fr` track is never smaller than what is in it, as CSS's `1fr` means
`minmax(auto, 1fr)`. Items with a `grid-column` and `grid-row` are placed
first, then items held to a row, then the rest flow into the next free cell
row by row, and rows or columns past the template are added as `auto`
tracks. Items fill their cells unless `justify-items`, `align-items` or an
item's `align-self` says otherwise, and `width: auto` sizes a grid to its
tracks. Not yet: `minmax()`, named lines and areas, `auto-fill`/`auto-fit`,
dense packing, and spanning items sizing the tracks they cross.

Text is measured by its font. A text element with `width: auto` or
`height: auto` is sized to its words, measured by the same shaping that draws
them (with no width limit when the width fits, so a label is one line), plus
its padding. That size is also its minimum when a layout shrinks it. So a
button that fits its content grows with its label:

```css
.badge { width: auto; height: auto; padding: 12px 18px; }
.badge-label { width: auto; height: auto; }
```

Measuring needs the font, so it is the host's to do: the game, the browser
export, the editor's views and the showcase's capture all measure each draw
(`sindri_scene::measure_ui_text`) and hand the sizes to layout, drawing and
hit-testing. A wrapping text's minimum is still its whole measured width, not
its longest word.

`padding` and `margin` take one to four values in CSS order (all; vertical
and horizontal; top, horizontal and bottom; top, right, bottom and left), and
the language expands them into their per-side longhands where they are
written, so a later `padding-top` overrides one side and keeps the rest. A
percentage on any side is of the containing element's content width, as in
CSS. Both are written to the element's `sindri.ui.box`, which the engine's
layout reads, so an authored scene gets the same box model without Weave.
As in flexbox, margins do not collapse: neighbours' margins and the `gap` add
up. Negative margins are accepted and currently count as none.

`gap` measures edge-to-edge space, not distance between child centres.
`space-between` places the first and last child at the content edges and
distributes remaining space between the interior gaps.

### Shape properties

`background`, `border-color`, `border-width`, `border-radius` and `box-shadow`
are listed in the property reference (in `08-weave-ui.md`).

These declarations target `sindri.ui.shape`.

`box-shadow` draws the shape's silhouette behind it, grown by the spread,
moved by the offset (positive `y` is down, as in CSS) and blurred over the
blur radius, with corners rounded to match. With no colour it is black at
half opacity. One shadow per element: a comma-separated list and `inset` are
refused rather than half drawn. A button can carry both
`sindri.ui.button` and `sindri.ui.shape`, allowing its hit target and visual
box to remain the same entity.

Colors accept `transparent`, `black`, `white`, and 3-, 4-, 6-, or 8-digit
hex. Hex colors are authored in sRGB and converted to the renderer's linear
space; alpha is unchanged.

### Text properties

`color`, `font-size`, `line-height`, `letter-spacing`, `font-weight`,
`font-style`, `text-align`, `text-wrap` and `text-transform` are listed in the
property reference (in `08-weave-ui.md`).

Wrapping requires a finite text width and enough height for the expected lines.
Use `word` for ordinary copy and `glyph` only when a long unbroken token must
break.

### Responsive rules

```css
@media (orientation: portrait) {
    .actions {
        direction: column;
        width: 100%;
    }
}

@media (max-width: 700px) {
    .title { font-size: 28px; }
}
```

Supported conditions are:

- `orientation: portrait`
- `orientation: landscape`
- `max-width: <length>`, `min-width: <length>`
- `max-height: <length>`, `min-height: <length>`

Combine them as in CSS: `and` requires every condition, a comma accepts any
of the alternatives, and a `@media` block nested in another applies only
where both do.

```css
@media (min-width: 900px) and (orientation: landscape) { … }
@media (max-height: 500px), (max-width: 400px) { … }
```

Put shared declarations in a normal rule and override only what changes inside
the media rule.

### Runtime behavior

The host loads the complete `.weave` source graph, composes each independent
entry stylesheet, computes cascade winners, then resolves a presentation world
for the viewport. Parents settle before descendants so percentages, padding,
and constraints have stable inputs. Rendering, pointer hit-testing, editor
placement, and generic UI layout consume the resulting ordinary Sindri
components.

A viewport change resolves a fresh presentation world from the authored scene.
Weave does not progressively mutate the previous resolved result.

Decay should continue referring to stable entity IDs:

```text
Ui.set_number("hud-score", score)
Ui.set_fill("hud-hp", health_ratio)
```

Presentation changes do not require those bindings to change.

### Transitions

`transition` eases a property from the value it is showing to a new one when
the rule that sets it changes, as in CSS:

```css
button { background: #334155; transition: background 150ms ease-out; }
button:hover { background: #475569; }
button:active { background: #1e293b; transition: background 50ms linear; }
```

The shorthand takes the property (or `all`), a duration, an optional easing
and an optional delay, and a comma-separated list for several properties.
Durations are `s` or `ms`. Easings are `linear`, `ease`, `ease-in`,
`ease-out`, `ease-in-out` and `cubic-bezier(x1, y1, x2, y2)`. Colours, lengths
and plain numbers ease; other values change at once. As in CSS, the
transition that applies is the one on the rule being changed *to*, and an
interrupted transition starts again from the value it had reached.

### Pointer states in the running game

`:hover` and `:active` follow the pointer in the editor's Play, native games
and browser exports. `:hover` matches the element under the pointer and every
container it is inside, and `:active` matches the pressed element while the
pointer stays on it (and a slider for as long as it is dragged), as in CSS.
Hit-testing uses what was drawn, so a button a media query moved or resized
is clicked where it is drawn. `:focus` follows the keyboard in the same hosts.
`:disabled` and `:checked` come from the entity's own data.

A running game has two layers, as a page in a browser does. The stylesheet's
rules are settled into the game's world when it starts and whenever the
screen changes shape, so scripts read styled values, and a value a script
then writes stays written, as an inline style beats a stylesheet. Each frame
only what pointer states and running transitions change is laid over that for
the draw, and taken off again before the scripts run; with nothing hovered
and nothing easing, a frame does no styling work at all.

### Inspecting styles in the editor

Select a UI element and the inspector's **Styles** section shows what Weave
did to it, as a browser's devtools do:

- the box model as drawn: margin, padding and size, in the stylesheet's
  pixels at the size the scene is presented at;
- every rule that matched, strongest first, with the file and line it was
  written on; a declaration a stronger rule, or a later stylesheet,
  overrode is struck through;
- the computed values the element ends up with, inherited ones included,
  and the custom properties in scope.

It follows the Game view's size, so switching the device preset shows the
rules its media queries choose. Click a declaration's value to change it:
Enter or clicking away writes the new value into the stylesheet on the
rule's line, leaving the rest of the file as it was, and the styles reload
from the file; Escape leaves it alone. A value that comes from a shorthand
(`padding-top` from `padding`) is refused rather than guessed at; edit the
shorthand. While the game is playing or paused, the Game view's **Pick**
toggle turns the next click in the game into a selection: the frontmost
element under it is selected, and the game does not receive the click.

`sindri_weave::inspect` answers the same questions for other tools, and
`weave::set_declaration` makes the same edit to a stylesheet's source.

### Authoring guidance

The editor recognizes manifest-listed `.weave` roots, previews their source,
authors `weave.style.classes`, and applies the composed result to Scene and Game
views. It hot-reloads saved changes throughout each root's `@use` graph. A
broken reload keeps the last good presentation and reports the source path,
line, and column. The Game view's device presets choose the viewport, and a
single value can be changed from the Styles section; writing stylesheets
otherwise happens in a text editor.

- Keep semantic identity in entity IDs and reusable appearance in classes.
- Keep one small entry stylesheet and split large surfaces with `@use`.
- Use scene parenting to describe layout ownership.
- Give layout children explicit boxes.
- Prefer content-driven authored dimensions over filling the viewport merely
  because `vh` is available.
- Test at least one wide, one narrow landscape, and one portrait viewport.
- Preserve touch targets when shrinking visuals; a readable mobile layout is
  more important than fitting a desktop composition onto a phone.
- Treat the authored component values as a safe fallback.

### Errors and unsupported declarations

Invalid values for known properties return an `ApplyError` containing the
entity, property, and value. Unknown properties are currently ignored.

The following CSS concepts are not implemented:

- a wrapping text's longest word as its minimum, and text wrapping to the
  width a layout gave it
- borders per side, negative margins, and more than one or an `inset` shadow
- `align-content` other than its default, `row-gap`/`column-gap`, and
  reversed directions
- grid's `minmax()`, named lines and areas, `auto-fill`, and dense packing
- scrolling and clipping regions
- `min()`, `max()` and `clamp()`
- attribute selectors, and the `-of-type` pseudo-classes
- `:focus` in the running game: nothing takes focus until keyboard and
  gamepad navigation land
- `@keyframes` animation
- accessibility mapping
- integrated stylesheet source editing and named viewport preset controls

Do not silently imitate these features with unexplained fixed coordinates.
Document the required workaround or add the missing primitive at the generic
Sindri UI layer when a real game proves it is needed.

### Diagnostic checklist

When an element is misplaced:

1. Confirm the selector matches the intended entity ID, class, or element,
   and that every compound in a descendant selector has an ancestor to match.
2. Confirm the entity is parented under the expected layout entity.
3. Check whether a more specific or later rule overrides the property.
4. Give the child an explicit `width` and `height`.
5. Check the parent's resolved size, padding, direction, gap, and alignment.
6. Verify that the active media condition matches the actual layout viewport.

When text escapes its box:

1. Set a finite width.
2. Enable `text-wrap: word`.
3. Provide height for every expected line.
4. Use a deliberate `line-height`.
5. Test the longest runtime string, not only the authored placeholder.

When a button looks stretched or has malformed corners:

1. Keep the shape and button hit target on the same entity.
2. Use an absolute border radius for a rounded rectangle.
3. Avoid a radius near half the height unless a pill is intentional.
4. Verify that responsive rules change both dimensions consistently.


#### Relationship to gameplay tweening

Weave animation authoring follows CSS: computed-style changes trigger
`transition`, and future authored animations should use CSS-style `@keyframes`
and animation declarations. Decay gameplay uses managed `Tween` handles and
playback controls, documented in scripting (in `03-scripting-guide.md`).
Both use the engine's named easing curves. Sharing that math does not make UI
stylesheets depend on gameplay or turn Decay tweens into CSS declarations.
`@keyframes` remains unimplemented in this slice.

---

Source: `docs/generated/weave-reference.md`

## Weave properties

Every property a Weave stylesheet may declare for Sindri UI in this build of the engine,
read from the table the bridge applies declarations by. A property not listed here is
ignored.

The language itself (selectors, the cascade, `var()`, lengths, colors and responsive
rules) is described in `docs/weave-reference.md` (in `08-weave-ui.md`). A length is
`px`, `vw`, `vh`, `%`, a plain number in overlay units, or `calc(…)`; a color is
`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `transparent`, `black` or `white`.

The machine-readable form of this page is
`weave-reference.json` (`docs/generated/weave-reference.json` in the Sindri repository).

### Size and position

#### `width`, `height`

The element's size. `auto` and `fit-content` size a layout to its children, padding and gaps, and a text element to its measured words.

- **Values:** a length, `auto` or `fit-content`
- **Writes:** the transform's scale; a text element's `sindri.ui.text` `bounds`
- **Example:** `width: 120px;`

#### `min-width`, `max-width`, `min-height`, `max-height`

Limits on the size, which a layout also honors when it grows or shrinks the element. When the two conflict the minimum wins, as in CSS.

- **Values:** a length
- **Writes:** the transform's scale, and `sindri.ui.box` `min_size` or `max_size`
- **Example:** `min-width: 40px;`

#### `x`, `y`

Where the element is, replacing the position the scene gave it.

- **Values:** a length
- **Writes:** the transform's position
- **Example:** `x: 10px;`

#### `anchor`

Which point of the element its position names.

- **Values:** `center`, `top`, `bottom`, `left`, `right`, `top-left`, `top-right`, `bottom-left` or `bottom-right`
- **Writes:** `anchor` on `sindri.ui.image`, `sindri.ui.text` and `sindri.ui.shape`
- **Example:** `anchor: top-left;`

### Box

#### `padding`, `padding-top`, `padding-right`, `padding-bottom`, `padding-left`

Room inside the element's edge: a layout's children start inside it. A percentage on any side is of the containing element's content width.

- **Values:** a length of zero or more; `padding` takes one to four, as in CSS
- **Writes:** `sindri.ui.box` `padding`
- **Example:** `padding: 8px;`
- **Shorthand:** `padding` is expanded into its longhands where it is written, so a later longhand overrides one part of it.

#### `margin`, `margin-top`, `margin-right`, `margin-bottom`, `margin-left`

Room a layout keeps free around the element. A percentage on any side is of the containing element's content width.

- **Values:** a length, negative allowed; `margin` takes one to four, as in CSS
- **Writes:** `sindri.ui.box` `margin`
- **Example:** `margin: 8px;`
- **Shorthand:** `margin` is expanded into its longhands where it is written, so a later longhand overrides one part of it.

### Flex layout

#### `display`

Makes the element a flex line or a grid that lays out its children.

- **Values:** `flex` or `grid`
- **Writes:** adds `sindri.ui.layout` (flex) or `sindri.ui.grid` (grid), removing the other
- **Example:** `display: grid;`

#### `flex-direction`, `direction`

The axis a flex line runs along.

- **Values:** `row` or `column`
- **Writes:** `sindri.ui.layout` `direction`
- **Example:** `flex-direction: column;`

#### `justify-content`

How a flex line spreads its children along its direction.

- **Values:** `start`, `center`, `end`, `space-between`, `space-around` or `space-evenly` (`flex-start`/`left` and `flex-end`/`right` mean `start` and `end`)
- **Writes:** `sindri.ui.layout` `justify`
- **Example:** `justify-content: center;`

#### `align-items`

Where children sit across a flex line, or across a grid's rows.

- **Values:** `start`, `center`, `end` or `stretch` (`flex-start` and `flex-end` too)
- **Writes:** `sindri.ui.layout` `align`, and `sindri.ui.grid` `align_items`
- **Example:** `align-items: center;`

#### `flex-wrap`

Whether children that do not fit start a new line.

- **Values:** `wrap` or `nowrap`
- **Writes:** `sindri.ui.layout` `wrap`
- **Example:** `flex-wrap: wrap;`

#### `flex`

Shorthand for the three below, read as CSS reads it: `flex: 1` makes children share a line equally whatever their sizes.

- **Values:** `none`, `auto`, `initial`, or `<grow> [<shrink>] [<basis>]`
- **Writes:** `flex-grow`, `flex-shrink` and `flex-basis`, which it stands for
- **Example:** `flex: 1;`
- **Shorthand:** `flex` is expanded into its longhands where it is written, so a later longhand overrides one part of it.

#### `flex-grow`, `flex-shrink`

The element's share of a line's spare room, or of its shortfall.

- **Values:** a number, zero or more
- **Writes:** `sindri.ui.box` `grow` or `shrink`
- **Example:** `flex-grow: 2;`

#### `flex-basis`

The element's size along the line before growing or shrinking.

- **Values:** a length of zero or more, `auto` or `content`
- **Writes:** `sindri.ui.box` `basis`
- **Example:** `flex-basis: 50%;`

#### `order`

Where the element comes in its layout, lowest first.

- **Values:** a whole number
- **Writes:** `sindri.ui.box` `order`
- **Example:** `order: 2;`

#### `position`

`absolute` takes the element out of its parent's line or grid, which closes up as if it were not there; `x` and `y` then place it from the parent's middle, over or under its siblings.

- **Values:** `absolute`, or `static` (`relative` reads the same)
- **Writes:** `sindri.ui.box` `absolute`
- **Example:** `position: absolute;`

#### `align-self`

One child's own `align-items`.

- **Values:** `auto`, `start`, `center`, `end` or `stretch`
- **Writes:** `sindri.ui.box` `align_self`
- **Example:** `align-self: center;`

### Grid layout

#### `grid-template-columns`, `grid-template-rows`

A grid's columns or rows.

- **Values:** tracks: `1fr`, `auto`, lengths and `repeat(n, …)`, or `none`
- **Writes:** `sindri.ui.grid` `columns` or `rows`
- **Example:** `grid-template-columns: 1fr 2fr;`

#### `gap`, `row-gap`, `column-gap`

Space between a grid's tracks. A flex line takes the gap along it.

- **Values:** a length of zero or more; `gap` takes a row gap and a column gap
- **Writes:** `sindri.ui.grid` `gap`, and `sindri.ui.layout` `spacing`
- **Example:** `gap: 8px;`

#### `justify-items`

Where grid items sit across their cells.

- **Values:** `start`, `center`, `end` or `stretch`
- **Writes:** `sindri.ui.grid` `justify_items`
- **Example:** `justify-items: center;`

#### `grid-column`, `grid-row`

Where an item starts in a grid, and how many tracks it spans.

- **Values:** `2`, `span 2`, `2 / 4`, `2 / span 3` or `auto`
- **Writes:** `sindri.ui.box` `grid_column` or `grid_row`
- **Example:** `grid-column: 2 / span 2;`

### Text

#### `color`

The text's color.

- **Values:** a color
- **Writes:** `sindri.ui.text` `color`
- **Example:** `color: #ff8800;`
- **Inherited:** an element without its own value takes its parent's.

#### `font-size`, `line-height`, `letter-spacing`

The glyphs' size, the distance between baselines, and extra space after every glyph.

- **Values:** a length
- **Writes:** `sindri.ui.text` `font_size`, `line_height` or `letter_spacing`
- **Example:** `font-size: 24px;`
- **Inherited:** an element without its own value takes its parent's.

#### `font-weight`

The font's own bold face, or its regular one.

- **Values:** `normal`, `400`, `bold` or `700`
- **Writes:** `sindri.ui.text` `bold`
- **Example:** `font-weight: bold;`
- **Inherited:** an element without its own value takes its parent's.

#### `font-style`

The font's own italic face, or its upright one.

- **Values:** `normal` or `italic`
- **Writes:** `sindri.ui.text` `italic`
- **Example:** `font-style: italic;`
- **Inherited:** an element without its own value takes its parent's.

#### `text-align`

Where each line sits across the text's box.

- **Values:** `start` (or `left`), `center`, `end` (or `right`) or `justify`
- **Writes:** `sindri.ui.text` `line_align`
- **Example:** `text-align: center;`
- **Inherited:** an element without its own value takes its parent's.

#### `text-wrap`

Whether lines break, and where: between words, or between any two glyphs.

- **Values:** `none` (or `nowrap`), `word` (or `wrap`) or `glyph`
- **Writes:** `sindri.ui.text` `wrap`
- **Example:** `text-wrap: word;`
- **Inherited:** an element without its own value takes its parent's.

#### `text-transform`

The case the text is shown in, leaving the text itself as written.

- **Values:** `none`, `uppercase` or `lowercase`
- **Writes:** `sindri.ui.text` `case`
- **Example:** `text-transform: uppercase;`
- **Inherited:** an element without its own value takes its parent's.

### Shape

#### `background`

The element's fill.

- **Values:** a color
- **Writes:** `sindri.ui.shape` `fill`
- **Example:** `background: #1f2937;`

#### `border-color`

The color of the element's outline.

- **Values:** a color
- **Writes:** `sindri.ui.shape` `stroke`
- **Example:** `border-color: #ffffff;`

#### `border-width`, `border-radius`

The outline's thickness, and how round the corners are.

- **Values:** a length in `px`, `vw` or `vh`, a percentage, or a plain number as a fraction of the element's shorter side
- **Writes:** `sindri.ui.shape` `stroke_width` or `corner_radius`
- **Example:** `border-width: 4px;`

#### `box-shadow`

A shadow behind the element's shape.

- **Values:** `<x> <y> [<blur> [<spread>]] [<color>]`, or `none`; one shadow, no `inset`
- **Writes:** `sindri.ui.shape` `shadow`
- **Example:** `box-shadow: 0 4px 8px #00000080;`

### Motion

#### `transition`

Eases a property to a new value instead of jumping, for example on `:hover`. Colors, lengths and plain numbers ease; anything else changes at once.

- **Values:** comma-separated `<property> <duration> [<delay>] [<easing>]`; a property may be `all`, a duration is `ms` or `s`, and an easing is `linear`, `ease`, `ease-in`, `ease-out` or `ease-in-out`
- **Writes:** nothing itself: the properties it names ease to each new value
- **Example:** `transition: background 150ms ease-out;`
