pathland-render-html 0.2.1

Pathland HTML renderer: maps opcode frames onto declarative HTML elements
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# pathland-render-html (Rust) — implementation status

**Last updated:** November 7, 2026

The **server-side / remote-projection HTML renderer**: a **stateless, streaming**
pure function of the opcode stream producing declarative HTML. Each render call
decodes a self-contained snapshot batch into a **transient** map, walks it once,
and emits HTML text out — no retained tree, zero cross-call state (Renderer
Statelessness). Protocol contract: `spec/`.

> **JVM note:** the Spring Boot / Quarkus paths no longer load this crate through
> JNA. `lib/java/pathland-render-html` is a **pure-Java behavioural port**, and
> `GoldenConformanceTest` keeps it byte-identical to this canonical renderer.
> The Rust crate remains the cross-language (C ABI) implementation and the
> source of the committed golden fixtures.

## Implemented

- **Window metadata → head**: `TITLE` → `<title>`, `DESCRIPTION` → `<meta name="description">`, `LOCALE` → `<html lang>` (default `en`/`Pathland`); values escaped. Also: generic `LABEL` → `aria-label` (icon buttons), slider names on the inner input, `aria-hidden` on the non-visual media shell.
- **SEO document metadata (issue #45)**: `DocumentMeta` is a per-render host
  option (`render_document_with_meta` / `render_document_linked_with_meta`) for
  the canonical URL, Open Graph/Twitter cards, and JSON-LD. Window
  `TITLE`/`DESCRIPTION` still win; `DocumentMeta` title/description are
  fallbacks. JSON-LD derives a `WebPage` plus one `Article` per `ROLE_ARTICLE`
  (headline from the first heading, description from the first paragraph, image
  from the first `IMAGE`); `<`/`>`/`&` are unicode-escaped so app text cannot
  break out of the script element. `pathland_html_render_with_meta` exposes the
  same option over the C ABI (`PathlandHtmlMeta`). The canonical Rust output is
  pinned by `document_meta.document.html` and the Java port reproduces it
  byte-for-byte.

- **`PROXY_VIEW` (0x1E)**: renders as a plain container box (a flex column,
  like a stack with one child) and lays out the (id-remapped) origin subtree the
  proxying server spliced in — the renderer holds no notion of "remote"
  (spec/PROXY.md §5). Without this arm the component fell to the `_ => String::new()`
  default and the spliced subtree was dropped. Covered by a unit test.
- **Attributed text**: `SET_ATTRIBUTED_TEXT` decodes the binary payload into the
  node model and renders non-overlapping `<span style>` runs plus `<a href>`
  links (v1 attributes: font size/weight/color/style/design/underline/
  strikethrough); text and URLs are escaped, and the plain text remains the
  fallback. Covered by a unit test.
- **Stateless streaming API**: `HtmlRenderer::render_document(opcodes, strings, root)`
  / `render_fragment(...)` build a transient decode map per call and stream HTML;
  the retained `apply`/`apply_frame`/`render` model is removed.
- **All spec components render**: `TEXT`, `IMAGE`, `COLOR` (layout-greedy:
  `flex:1;align-self:stretch`), `SHAPE` (CSS/SVG by `SHAPE_KIND`), `DIVIDER`, `SPACER` (inline
  `flex:1`), `PROGRESS_VIEW`/`GAUGE` (`data-min`/`data-max` ride on the gauge so the DOM
  client can recompute its percentage), `VSTACK`/
  `HSTACK`/`LAZY_VSTACK`/`LAZY_HSTACK` (flex), `ZSTACK` (overlay), `GRID`/
  `LAZY_VGRID`/`LAZY_HGRID` (CSS grid), `SCROLLVIEW`, `BUTTON`, `TEXT_FIELD`
  (incl. `IS_SECURE` → `type="password"`), `TEXT_EDITOR`, `TOGGLE`
  (`TOGGLE_STYLE`), `SLIDER`, `STEPPER` (the `.pathland-stepper` composite with
  `data-min`/`data-max`/`data-step` on its hidden range span), `DATE_PICKER` (`SET_DATE` →
  `days_to_date`), `PICKER` (option children rendered as `<option data-pathland-id="{child}"
  value="{index}">` + `SELECTION`), `MENU` (the `.pathland-menu` composite:
  `.pathland-menu-trigger` label + `.pathland-menu-items` children),
  `COLOR_PICKER`, `COMMENT`.
- **Native elements** (the specs carry no renderer mappings — this is the
  concrete HTML map the removed spec hints lived in): `TEXT` → `<span>` (or
  `<p>`/`<h1>`–`<h5>` by `ROLE`/heading `TEXT_STYLE`), `IMAGE` → `<img>`,
  `ICON` → inline **Remix (filled)** `<svg class="pathland-icon">` (the canonical
  `ICON_NAME` resolves to its filled SVG markup via this renderer's own
  `icons` module (`crate::icons`, the web owner of the mapping); the svg carries
  `data-pathland-icon="<name>"` — the DOM client's lazy resolver reads it as an
  embedded glyph for instant on-page swaps; `LABEL` →
  `role="img"` + `aria-label`, none → `aria-hidden`; sized `1em` so `FONT_SIZE`
  scales it, tinted via `fill="currentColor"`),
  `SIZE_THAT_FITS` → `<div class="pathland-ftf">` (the fit slot: fill-sized with
  `data-pathland-fit` mirroring the `FIT_QUERY` LIST, decoded as `[count][f32×count]`
  at decode time — the DOM client derives the fit locally and reports
  `FIT_CHANGED` on transitions; the slot is layout-transparent, forwarding the
  parent flex container's `flex-direction`/`align-items`/`justify-content`
  (`inherit`) into its own box so a wrapper never pins a centered candidate to
  the start — `VStack(CENTER)` > fit slot > `HSTACK` keeps centering; stays
  size-taking via `flex:1 1 auto;align-self:stretch;min-width/height:0`),
  and the **web-SSR snapshot form** (issue #141: the slot carries EVERY candidate,
  in `FIT_QUERY` order — the protocol's candidate-identity contract) renders each
  branch in a `data-pathland-fit-cand="i" data-pathland-fit-cand-id="<child>"`
  gate wrapper whose inline `display` is the pre-CSS default (candidate 0
  `contents`, every other candidate `none`), marks the slot as the query context
  (`container-type:inline-size; container-name:pf-<id>`), and emits the
  `@container` **band rules** into a `<style data-pathland-fit>` head element: for
  each distinct threshold `T_i` (`i ≥ 1`, ascending) a self-contained
  `@container pf-<id> (min-width: T_i)` band that **hides every candidate `j ≠ i`
  and shows candidate `i`**, so the highest matching band wins and **exactly one**
  candidate is visible at any width (no band for `T_0`; below the first band the
  inline default shows candidate 0). A no-JS first paint therefore already shows
  the candidate matching the slot's width; the single-child live form is
  unchanged. The bands are **SSR-only** — the first `FIT_CHANGED` collapses the
  slot to the live single-child form and the DOM client removes the gating
  (`collapseFitSlotFor` unwraps the gate wrappers, strips the container/candidate
  attrs, and `dropFitBandRules` deletes the slot's rules)),
  `COLOR` → `<div>`, `SHAPE` → CSS shapes / inline SVG, `DIVIDER` → `<hr>`,
  `SPACER` → inline flex filler, `PROGRESS_VIEW` → `<progress>` /
  `.pathland-spinner`, `GAUGE` → `.pathland-gauge`, stacks/lazy stacks → flex
  `div`, `ZSTACK` → grid overlay `div`, grids → CSS-grid `div`, `SCROLLVIEW` →
  overflow `div` (`SCROLL_AXIS` 0x0021: greedy on the scroll axis only; the
  cross axis hugs or fill-propagates), `BUTTON` → `<button>`, `TEXT_FIELD` → `<input>` (secure →
  `type="password"`), `TEXT_EDITOR` → `<textarea>`, `TOGGLE` → checkbox/switch
  input, `SLIDER` → `<input type="range">`, `STEPPER` → `.pathland-stepper`,
  `DATE_PICKER` → `<input type="date">`/`<input type="time">`, `PICKER` →
  `<select>` + `<option>`, `MENU` → `.pathland-menu` composite, `COLOR_PICKER`
  → `<input type="color">`, `VIDEO` → `<video controls>`,
  `MEDIA_CONTROLLER` → an off-flow (`position:absolute;1px;opacity:0`) shell
  (non-visual, layout-transparent — **not** `display:none`, which silences the
  `<video>`'s audio), `COMMENT` → none.
- **Media (spec/MEDIA.md)**: `IMAGE` renders `<img src alt>` (the `LABEL`
  accessibility text is
  the `alt`; empty = decorative) and honors `CONTENT_MODE`/`ASPECT_RATIO`
  inline (`Fit`→`object-fit:contain`, `Fill`→`object-fit:cover`); a `VIDEO`
  surface renders `<video data-pathland-controller="C" controls>` (the binding
  attribute appears only when the surface references a controller). **Audio
  has no view.** A **`MEDIA_CONTROLLER`** is non-visual and layout-transparent:
  it renders an off-flow shell carrying the resource
  (`data-pathland-media-controller` + `data-pathland-media-src`), which the DOM
  client turns into the playback (keyed by the controller id; spec/EVENTS.md
  Media). Asset refs are absolute (`/_pathland/assets/…`);
  bytes never ride the opcode stream.
- **Composite override mode**: `BUTTON`/`TOGGLE`/`SLIDER` with children render
  the custom body wrapped in the native element.
- **Native browser controls inherit the design system**: the Tailwind-derived
  preflight stays the base reset, and every native form control carries a
  `pathland-*` class styled from design tokens:
  - text fields/editors: `.pathland-input.pathland-control`;
  - `PICKER` → `.pathland-select.pathland-control` (native dropdown arrow kept,
    tokenized field + options);
  - `DATE_PICKER` / `COLOR_PICKER` → `.pathland-date` / `.pathland-color` with
    `.pathland-control` (tokenized surface, inherited `color-scheme`,
    `accent-color`);
  - `SLIDER` → `.pathland-slider` with a renderer-owned track/thumb
    (`slider.track` / `slider.accent` / `slider.thumb`), no native range chrome;
    the `--pl-slider-fill` custom property is emitted inline for first paint and
    kept in sync by the DOM client (value/min/max/input). The native range input
    remains for keyboard/pointer interaction and accessibility;
  - `PROGRESS_VIEW` → `.pathland-progress` with tokenized WebKit/Mozilla
    track/fill pseudo-elements;
  - `VIDEO` → `.pathland-video` (and `.pathland-media` for custom bodies) with
    surface/radius/accent tokens.
  Inline opcode properties still override the base classes. Native `<select>`
  popups, date/color dialogs, and `<video>` controls remain UA shadow DOM, so
  their internals are best-effort; app-driven media is fully themeable.
- **Properties**: `ALIGNMENT` (cross-axis **position** — the default is hug
  (`flex-start`), never CSS `align-items:stretch`, per `spec/LAYOUT.md`; only
  `FILL`-sized children stretch via their `100%` size), `CONTENT_MARGINS`,
  `WIDTH`/`HEIGHT` (`FILL` → `width:100%`/`height:100%` expansion, `HUG` →
  intrinsic), the frame bounds `MIN_WIDTH`/`MAX_WIDTH` and the height
  equivalents (emitted on the SSR first paint, matching the DOM client;
  `IDEAL_*` was retired),
  `PADDING` + per-edge, `COLOR`,
  `BACKGROUND_COLOR`, `FONT_SIZE`/`WEIGHT`/`FAMILY`, `TEXT_STYLE` (the
  renderer-owned default size/weight; explicit `FONT_SIZE`/`WEIGHT` override),
  `OPACITY`, `VISIBLE`,
  `Z_INDEX`, `LINE_LIMIT` (positive → `-webkit-box`/`-webkit-line-clamp` **with
  `overflow:hidden`** — the clamp draws the ellipsis but does not clip on its own,
  so Safari would otherwise paint the overflowed text below the box, matching
  the DOM client), `TEXT_ALIGNMENT`, `TRUNCATION_MODE`, `BORDER_*`,
  `SELECTED`, `TOGGLE_STYLE`, `ENABLED`, `ROLE`/`STATE` (ARIA), plus
  `TEXT_CASE`, `FONT_STYLE`, `FONT_DESIGN`, `UNDERLINE`/`STRIKETHROUGH`,
  `CLIPS_TO_BOUNDS`, `ALLOWS_HIT_TESTING`, `COLOR_INVERT`. `WIDTH`/`HEIGHT`
  `HUG_CONTENT` (-2) is **omitted** (intrinsic size); `FILL` (-1) → `100%`.
- **`SHADOW_*` (box-shadow)**: a non-zero `SHADOW_RADIUS` (or an `SHADOW_X`/`SHADOW_Y`
  offset) emits `box-shadow: <color> <x>px <y>px <radius>px` (color first — the
  canonical order a browser serializes back to, so the SSR string matches the
  hydrated DOM). The color is
  `SHADOW_COLOR` when set, else the **renderer-owned default `rgba(0,0,0,0.3)`** —
  so a `shadow(radius)` modifier (radius only; the color is renderer-owned) already
  draws before hydration, matching the DOM client.
- **`Z_INDEX` (draw-order override)**: a `ZSTACK` child's `Z_INDEX` is emitted on
  its **shell** (the grid item that participates in stacking — the child's own
  `z-index` is inert inside the shell), so a higher value draws above lower
  siblings regardless of child index (spec/PRIMITIVES.md §ZStack). **Every shell
  carries an explicit `z-index`** (the child's `Z_INDEX`, else `0`) and the ZStack
  container sets `isolation:isolate`, so overlapping grid items paint
  deterministically in every browser (WebKit paints a non-fill grid item below a
  full-area sibling when both are `z-index:auto`). Mirrored by the DOM client's
  `applyLayout`.
- **Layout contract (spec/LAYOUT.md)**: cross-axis default is **hug**
  (`flex-start`), not CSS stretch; `ALIGNMENT` positions only (`Fill`=3 → hug
  default). `DIVIDER` is greedy on the cross axis (`width:100%`);
  `SCROLLVIEW` is greedy on its `SCROLL_AXIS` only; fixed frames emit exact px
  boxes. Finite `MAX_*` bounds cap the resolved size, stop unbounded fill
  propagation, and let the parent's `ALIGNMENT` position the capped box.
  Conformance cases C1–C6 and C13–C20 asserted in `lib.rs` tests.
- **Fill propagation (spec/PRIMITIVES.md §stack layout model)**: implemented —
  `fills_axis` resolves a Hug stack/ZStack containing a `FILL` child / greedy
  primitive on an axis as `FILL`, emitting `width/height:100%` on that axis, and
  cross-axis `FILL` children stretch via `align-self:stretch` (SSR + TS DOM
  client, kept canonical via the layout pass in `applyLayout`).
- **ZStack (spec/PRIMITIVES.md)**: implemented — an overlapping grid
  (`grid-area:1/1` cells); the container hugs to its largest child (`max-content`)
  unless Fixed/FILL (or fill propagation → `100%`); each child keeps its own
  size and is positioned by `ALIGNMENT` on both axes (SSR + TS DOM client).
  **Grid tracks are `minmax(0,1fr)`** (ZStack + count-generated grid tracks,
  `repeat(n,minmax(0,1fr))`, and lazy-hgrid auto columns), so a FILL ZStack/grid
  can shrink below its content min-height on a short viewport — an `end`-aligned
  child (e.g. a player bar) stays pinned instead of being pushed out of view and
  the inner `ScrollView` scrolls. A `GRID_TRACKS` per-track spec is emitted
  verbatim (app-authored). **Child shells carry no explicit
  `width/height`** — `justify-self/align-self:stretch` only applies when a grid
  item's size is `auto`, so pinning `max-content` silently degraded `stretch` to
  `start` and a FILL child's `100%` resolved circularly (content height),
  overflowing the viewport. With `auto` sizes a FILL child genuinely stretches
  to its (shrinkable) track and a Hug child keeps content size + alignment.
- **ARIA ROLE/STATE maps match the DOM client**: the full `ROLE` set (button…
  menu, incl. `text`/`img`/`radio`/`spinbutton`/`tablist`/`list`/`grid`/
  `region`/`menu`) and the `STATE` semantics (one true `aria-*` per state) are
  canonical with `lib/typescript/src/classes.ts`.
- **Semantic HTML elements from `ROLE` + typography** (spec/OPCODE.md §semantic
  properties): a semantic role retags a **generic `div`/`span` shell** to its
  native element — `HEADER`→`<h2>`, `PARAGRAPH`→`<p>`, `LIST`→`<ul>`,
  `LIST_ITEM`→`<li>`, `SUMMARY`→`<section>`, and the landmarks `BANNER`→
  `<header>`, `NAVIGATION`→`<nav>`, `MAIN`→`<main>`, `CONTENT_INFO`→`<footer>`,
  `COMPLEMENTARY`→`<aside>`, `ARTICLE`→`<article>`, `SECTION`→`<section>`,
  `SEARCH`→`<search>`. **Control components keep their native element** and
  emit no ARIA role from `ROLE` (`BUTTON` stays a `<button>`, `MENU` renders
  its intrinsic `role="menu"`). **Headings** come from a heading `TEXT_STYLE`
  typography (LargeTitle…Headline → `<h1>`–`<h5>`, always a heading) or from
  `ROLE_HEADER` (default `<h2>`); non-heading text defaults to `<span>`.
  Interactive/control roles are NOT in the catalog (intrinsic to the
  components); a custom button uses `Button`+`ButtonStyle`. The mapping lives
  in **`src/role_spec.rs`** (canonical with the DOM client —
  `pathland-ts-codegen` emits `generated/role-spec.ts`), with the role codes
  centralized in `pathland_core::constants::role`.
- **`TREE::INSERT_CHILD` honors its `C` index** (`u32::MAX` = append), matching
  the protocol and the DOM client's `insertAt`.
- **Event surfacing**: `data-event-listeners` / `data-action-id` /
  `data-binding-id` attributes.
- **Navigation slot attrs** (spec DSL.md §4.5 / MODIFIERS.md): a slot node's
  `ROUTE` (STRING) renders as `data-pathland-route="<path>"`, its
  `TRANSITION` hint renders as `data-pathland-transition="<platform|fade|slide|scale>"`,
  its `NAV_CHROME` (F32 enum) renders `data-pathland-nav-chrome="custom"`
  when `Custom` (the renderer adds no default chrome), and `NAV_DEPTH` (U32)
  renders `data-pathland-depth="N"` when deeper than its root so the DOM client
  can hydrate its default back button from the SSR HTML — the DOM renderer
  mirrors the route into the URL, may animate a swap, and draws the renderer's
  own back button (the web has no native navigation container).
- `PARAMETER::SET_DATE` handled (days + millis-of-day → date/time).
- **Design tokens (spec/TOKENS.md)**:
  - The naming/length conventions live in **`src/token_spec.rs`** — the single
    source of truth consumed by the renderer AND by `pathland-ts-codegen`, which
    emits the DOM client's `lib/typescript/src/generated/tokens-core.ts`
    (`tokenToCssVar`, `isDarkToken`, `isLengthToken`, `resolveTokenCssRef`).
  - `PARAMETER::SET_DESIGN_TOKEN` overrides are collected per snapshot batch and
    emitted into the document head as CSS: base (light) tokens as `:root`
    rules, `dark.*` overrides inside `@media (prefers-color-scheme: dark)` —
    the browser resolves the scheme natively, so SSR needs no client scheme.
  - `DESIGN_TOKEN`-typed `SET_PROPERTY` values resolve at render time to
    `var(--pl-…)`, with the generative `space.<N>` family resolving to
    `calc(var(--pl-space-base) * N)` (`Node::token_ref`).
  - **`STRING`-valued token overrides** (e.g. `font.body.family`): a
    `SET_DESIGN_TOKEN` with `valueType = STRING` resolves the value string from
    the batch's string section and emits it single-quoted (escaped) as a
    `:root` rule (or inside the dark media query).
  - The override rules render inside their own **`<style data-pathland-tokens>`**
    element in the document head, **after** the built-in block (so their
    `:root` variables win the cascade and the browser applies them) — matching
    the JS DOM client's `style[data-pathland-tokens]` element. No overrides →
    no extra element.
  - The built-in `:root` tokens use the **canonical spec paths** (`--pl-color-primary`,
    `--pl-color-background`, `--pl-color-text-primary`, `--pl-color-text-secondary`,
    `--pl-color-surface`, `--pl-color-border`) so app overrides retheme the
    renderer.
  - **Full Tier-1 default coverage**: `--pl-space-base` (light + dark, so the
    generative `space.<N>` family always resolves), canonical typography
    (`--pl-font-body-size/weight/family`), the radius scale + `--pl-border-width-thin`,
    the **shared control core** (`--pl-control-*`, `--pl-button-*`,
    `--pl-input-*`), and the component tokens (`--pl-toggle-*`,
    `--pl-slider-*`, `--pl-progress-*`, `--pl-gauge-*`, `--pl-stepper-*`,
    `--pl-picker-*`, `--pl-menu-*`, `--pl-label-*`, `--pl-size-control-*`),
    all with light + dark values. `.pathland-*` rules reference the canonical
    variables, so `SET_DESIGN_TOKEN` overrides retheme components.
  - **Forced scheme**: `META::SET_COLOR_SCHEME` sets `data-pathland-scheme` on
    `<html>`; the platform-dark media rule is scoped to
    `:root:not([data-pathland-scheme])` and the dark values are repeated under
    `:root[data-pathland-scheme="dark"]`. Forced light suppresses the media
    rule. `META::RESET_THEME` clears the batch's overrides.
  - **Explicit `dark.` references** resolve to the forced-dark variable family
    (`--pl-dark-…`), so they use the dark value in either scheme.
  - **Unknown/unresolved token fallback**: `var(--pl-…, unset)` so the
    renderer's own default styling applies. Base overrides are repeated in the
    dark scope so they correctly outrank built-in dark defaults (dark override
    → base override → dark default).

## Built-in HTML base design (`design/`)

The built-in stylesheet is now **generated**, not hand-written in `css.rs`:

- `design/tokens/{base,semantic,component}` is a three-layer Style Dictionary
  source (flat DTCG entries). Base literals are compiled into literals and never
  emitted; semantic + component tokens are emitted in four layers (`:root`,
  platform-dark media, forced-dark/light scheme selectors, and the
  `--pl-dark-*` forced-dark family).
- `design/html/{reset,components}.css` holds the hand-authored reset and
  `.pathland-*` rules. Component rules consume `var(--pl-*)` tokens; no
  design-relevant literal remains (sizes, radii, border widths, durations,
  z-index, focus rings, opacities).
- `design/scripts/{build,dev,check}.mjs` provide build/watch/validation.
  `check` verifies Tier-1 catalog coverage, layer/reference direction,
  dark/forced-dark declarations, `$extensions.pathland.internal` markings, and
  that every `$type: dimension` token is recognized by `token_spec::isLengthToken`.
- `design/build/base.css` + `base.css.name` (the SHA-256 content hash, e.g.
  `base-5f86423db422.css`) are synced to `assets/` by
  `scripts/sync-design-css.sh` (and `--check` fails on drift). `src/css.rs`
  embeds both with `include_str!`; `builtin_css()` / `builtin_css_name()` expose
  them and `STYLE` wraps the bundle in `<style>`.
- **External stylesheet mode**: `render_document` stays inline (the library
  default); `render_document_linked(opcodes, strings, root, css_base)` emits
  `<link rel="stylesheet" href="{css_base}/{name}">` instead. Token overrides and
  fit-band rules stay inline after it. The C ABI adds
  `pathland_html_render_linked`, `pathland_html_base_css`, and
  `pathland_html_base_css_name`; the Java port + Spring/Quarkus starters expose
  them and serve the CSS immutable. An empty base falls back to inline.
- The design bundle is wired into the Maven `refresh-ide` profile and a CI
  design job that runs build/check/sync-check.

## Design decision (decision A, September 2026): inline-all + built-in design system

Tailwind is **removed**. The renderer emits **everything inline** as CSS in a
single `style` attribute — no external compiler, no class system, no safelist:

- **All opcode properties render inline** (`style_css()` + `border_style()`),
  including the previously class-based enum surface (alignment, text alignment,
  text case, visible, font style/design, underline/strikethrough, clips-to-bounds,
  hit-testing, invert, truncation). **1 dp = 1 CSS px**.
- **`css::STYLE`** is the generated built-in `<style>` block injected by
  `render_document` (see "Built-in HTML base design" above), containing:
  - a **preflight reset** (vendored from Tailwind CSS, MIT — see
    `THIRD_PARTY_NOTICES`),
  - `:root` **design tokens** as CSS custom properties (`--pl-color-primary`,
    `--pl-color-background` (document background), `--pl-color-surface`,
    `--pl-color-text-primary`/`--pl-color-text-secondary`, `--pl-color-border`,
    `--pl-radius-*`, `--pl-font-sans`, `--pl-input-*` (field bg/text/outline/
    placeholder/focus), button tokens), named to match the canonical protocol
    design-token paths so `SET_DESIGN_TOKEN` can retheme the renderer,
  - `.pathland-button` (the prettified-button POC, with `prefers-color-scheme`
    dark mode) and `.pathland-*` component defaults (toggle switch, slider, text
    field/editor, stepper, spinner, gauge, menu) — the interactive/hover/focus/
    disabled states that can't be expressed inline. Text fields use a
    Tailwind-style **inset outline** (`outline: 1px solid`, `outline-offset: -1px`,
    focus → `2px`/`-2px` + indigo) instead of a border, with dark-mode tokens;
    `html` sets `color-scheme: light dark` + the `--pl-color-bg` background so
    native form controls and the page follow the theme.
- **`pathland_html_*` cdylib** (`pathland_html_render` / `_render_fragment` /
  `_free`, plus `pathland_html_icon_svg`) for non-JVM cross-language hosts
  (Swift/.NET/…; the JVM uses the pure-Java port).
  `pathland_html_icon_svg(name)` returns the icon glyph's full `<svg>`
  (canonical name → glyph, unknown/extension → the renderer's fallback glyph) —
  how any host serves `/_pathland/icons/<name>.svg` from ONE Rust source (the
  DOM client's lazy icon fetch) instead of per-platform static files. The
  `_tailwind_compile`
  entry point, `tw.rs`, `tailwind.rs`, `build.rs`, `scripts/fetch-tailwind.mjs`,
  `vendor/`, and the `tailwind-embed` feature are **deleted**.
  Non-JVM hosts that embed the cdylib must refresh it with `cargo build -p
  pathland-render-html` (not just `cargo test` — tests rebuild the rlib, not
  the shared library). The JVM no longer embeds it; `GoldenConformanceTest`
  guards the Java port against golden-fixture drift.
- **Debug comments** (`HtmlRenderer::with_debug_comments(true)`; C ABI
  `pathland_html_render_debug` / `_fragment_debug`): every rendered node is
  prefixed with an HTML comment naming its component type and the modifiers
  applied (`<!-- #1 VStack: spacing=4, alignment=Fill, width=FILL -->`) — values
  decoded by protocol type (FILL/HUG sentinels, well-known enums, colors,
  strings, bitmasks); opt-in so default output is unchanged, and comments stay
  valid HTML (content scrubbed of `--`).

## Cross-renderer conformance (no drift)

The **`pathland-html-golden`** crate renders a battery of scenarios to committed
fixtures under `lib/typescript/test/fixtures/ssr/` (`{name}.plpl` batch bytes +
`{name}.html` canonical `render_fragment`). Its `tests/guard.rs` fails if the
SSR output changes without the fixtures being regenerated
(`cargo run -p pathland-html-golden -- --emit`), and the TypeScript
`test/ssr-conformance.test.ts` asserts the DOM client reproduces the same DOM
from the same batches (fresh-DOM + hydrate-then-delta). The SSR output is the
contract both renderers must satisfy.

## Not implemented / gaps

- **`TRUNCATION_MODE`** (spec/LAYOUT.md §content fitting): aligned — it **alone**
  has no observable effect (the old `nowrap`+`text-overflow:ellipsis` emission is
  gone, SSR + TS DOM client); under a `LINE_LIMIT` clamp the renderer tail-
  ellipsizes via `line-clamp`. Renderer-owned fidelity: CSS cannot place a
  head/middle ellipsis, so non-Tail modes fall back to the tail ellipsis.
- **`DESIGN_TOKEN` property references** cover the directly-mappable subset
  (colors, font size/weight/family, spacing/padding/content margins, corner
  radius, opacity, width/height, border width/color, shadow color/x/y/radius).
  **Parent-path fallback and per-component native control chrome are not
  implemented in CSS** (see the gaps above).
- **Grid/ScrollView/Lazy layout (spec/PRIMITIVES.md §Grid / §ScrollView)**:
  grids emit `gap` from `SPACING`, equal-`1fr` track templates from the
  `GRID_COLUMNS`/`GRID_ROWS` constructor properties (columns for GRID/
  LAZY_VGRID, rows for LAZY_HGRID + `grid-auto-flow:column`), **`GRID_TRACKS`**
  per-track specs (`flex`/`fixed:<pts>`/`adaptive:<pts>` → `1fr`/`<pts>px`/
  `minmax(<pts>px,1fr)`, taking precedence over the count), and **per-cell
  alignment** — each cell is wrapped in an auto-placed shell whose
  `justify-self`/`align-self` is `stretch` for a `FILL`/greedy cell, else the
  grid `ALIGNMENT` position (default start) — with `justify-items`/
  `align-items` on the container. **`GRID_ROW`** (0x1D) rows are flattened with
  explicit `grid-row`/`grid-column` placement; each row renders as a transparent
  `<div style="display:contents">` (its id hydrates the DOM client), and a
  short row leaves trailing columns empty. `WIDTH`/`HEIGHT` on a grid are the
  universal pixel box, never a count. `SCROLLVIEW` is greedy on its
  `SCROLL_AXIS` only, carries `data-pathland-scroll-axis`, and renders **only
  its first child** (the spec-pinned single content). Conformance cases
  C11–C20 covered by the SSR tests + golden fixtures (incl. `gridrow`,
  `scrollaxis`, and `maxbounds`).
- `SHAPE` `Path` renders as an SVG placeholder (no path data wire property).
- GAUGE/SHAPE visuals are CSS approximations, not pixel-exact.
- **Parent-path token fallback is not implemented in CSS**: overriding a
  parent path (e.g. `font.body`) does not derive leaf variables
  (`font.body.size`). Override the leaf paths on the web.
- **Typography uses the platform system font stack** (`system-ui,
  -apple-system, 'Segoe UI', Roboto, …`) — no external/webfont loading, so the
  rendered document makes no network requests for fonts and works offline.
  An app can still pick its own typeface via a `font.body.family`
  `SET_DESIGN_TOKEN` override (any STRING value, e.g. `"Inter"` if the app
  hosts it).
- Compound token refs cover shadow x/y/radius and border width (the DOM client
  mirrors them); per-component tokens still style only the component CSS the
  renderer owns.

## Verified by

`cargo test -p pathland-render-html` — 77 headless render tests (components,
properties, composite, event attrs, navigation slot route/transition attrs,
`days_to_date`, network-decoded frames, inline-all styling, built-in CSS block
contents, Inter CDN head links, design tokens: base/dark override CSS, `px`
lengths, `DESIGN_TOKEN` refs, generative `space.N`). The built-in CSS tests
assert the generated bundle is embedded (reset, tokens, component rules) and
that component rules reference the catalog/metadata tokens; the linked-mode
tests assert the hashed `<link>` and that the default render remains inline. `cargo test -p
pathland-html-golden` — the SSR golden-fixture guard. `npm --prefix design run
build && npm --prefix design run check` and `scripts/sync-design-css.sh --check`
validate the design pipeline and the committed bundle.