{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "attribution": {
      "description": "Attribution for the style itself (HTML allowed). Merged with per-source and upstream attributions.",
      "type": "string"
    },
    "functions": {
      "additionalProperties": {
        "properties": {
          "description": {
            "type": "string"
          },
          "inputs": {
            "additionalProperties": {
              "properties": {
                "default": {},
                "description": {
                  "type": "string"
                },
                "kind": {
                  "enum": [
                    "features",
                    "raster",
                    "sprite",
                    "brush",
                    "scalar",
                    "scalar-field"
                  ]
                }
              },
              "required": [
                "kind"
              ],
              "type": "object"
            },
            "type": "object"
          },
          "nodes": {
            "$ref": "#/properties/nodes"
          },
          "output": {
            "description": "Body node (with or without `@`) the call produces.",
            "type": "string"
          },
          "output-kind": {
            "enum": [
              "features",
              "raster",
              "sprite",
              "brush",
              "scalar",
              "scalar-field"
            ]
          }
        },
        "required": [
          "output",
          "output-kind",
          "nodes"
        ],
        "type": "object"
      },
      "description": "User-defined functions: reusable node subgraphs called via `op: func`.",
      "type": "object"
    },
    "legend": {
      "description": "What the map's symbols mean. Never rendered into a tile — hosts read it to draw a legend beside the map.",
      "properties": {
        "entries": {
          "description": "Entries in reading order.",
          "items": {
            "properties": {
              "from": {
                "description": "The node that draws this symbol; must produce a Raster.",
                "type": "string"
              },
              "geometry": {
                "description": "Geometry the swatch's stand-in feature carries. Defaults to all three; name one when a geometry op between the source and this node would otherwise make the node draw twice.",
                "enum": [
                  "all",
                  "polygon",
                  "line",
                  "point"
                ]
              },
              "label": {
                "description": "What the reader sees.",
                "type": "string"
              },
              "max-zoom": {
                "minimum": 0,
                "type": "integer"
              },
              "min-zoom": {
                "minimum": 0,
                "type": "integer"
              },
              "note": {
                "type": "string"
              },
              "properties": {
                "description": "Feature properties selecting this entry's case.",
                "type": "object"
              }
            },
            "required": [
              "label",
              "from"
            ],
            "type": "object"
          },
          "type": "array"
        },
        "note": {
          "description": "Prose below the entries: sources, classification method, what the map leaves out.",
          "type": "string"
        },
        "title": {
          "description": "Heading — usually what is being mapped.",
          "type": "string"
        }
      },
      "type": "object"
    },
    "name": {
      "type": "string"
    },
    "nodes": {
      "additionalProperties": {
        "oneOf": [
          {
            "description": "Axis-aligned bounding box of every input vertex (pooled across polygons, lines, and points), emitted as a single rectangular polygon.",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "bbox",
                "description": "Selects the `bbox` operation."
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: bbox",
            "type": "object"
          },
          {
            "description": "Composite `over` onto `base` with a W3C blend mode. `clip: true` clips result to base alpha (Photoshop-style clipping mask, i.e. source-atop): the result *takes* the base's alpha, so clipping onto a 0.12-alpha wash caps the whole overlay at 0.12 — when you mean \"restrict this to an area\", give an opaque shape as `mask` instead. `composite: \"destination-out\"` makes `over` erase `base` (brush-eraser effect when `over` is a brush-shaped raster). Optional `mask` raster's alpha modulates source coverage.",
            "properties": {
              "base": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "clip": {
                "default": false,
                "oneOf": [
                  {
                    "type": "boolean"
                  },
                  {
                    "pattern": "^[$@].+",
                    "type": "string"
                  }
                ]
              },
              "composite": {
                "default": "over",
                "enum": [
                  "over",
                  "source-over",
                  "destination-out",
                  "dest-out",
                  "erase"
                ],
                "type": "string"
              },
              "mask": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "mode": {
                "default": "normal",
                "enum": [
                  "normal",
                  "multiply",
                  "screen",
                  "overlay",
                  "darken",
                  "lighten",
                  "color-dodge",
                  "color-burn",
                  "hard-light",
                  "soft-light",
                  "difference",
                  "exclusion",
                  "hue",
                  "saturation",
                  "color",
                  "luminosity"
                ],
                "type": "string"
              },
              "op": {
                "const": "blend",
                "description": "Selects the `blend` operation."
              },
              "opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "over": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              }
            },
            "required": [
              "op",
              "base",
              "over"
            ],
            "title": "op: blend",
            "type": "object"
          },
          {
            "description": "Gaussian blur on a raster (libblur, exact). Grows upstream pad by 3σ, so `sigma` needs an upper bound the build can see: a literal, a `$param` with `max`, or `sigma-max` alongside an `@node` port (the port's value is then clamped to it).",
            "properties": {
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "blur",
                "description": "Selects the `blur` operation."
              },
              "sigma": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "sigma-max": {
                "description": "Upper bound on `sigma` for padding, required when `sigma` is an `@node` port. Values above it are clamped.",
                "minimum": 0.0,
                "type": "number"
              }
            },
            "required": [
              "op",
              "input",
              "sigma"
            ],
            "title": "op: blur",
            "type": "object"
          },
          {
            "description": "Convert each polygon to its boundary rings (exterior + holes) as polylines. Existing polylines and points pass through.",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "boundary",
                "description": "Selects the `boundary` operation."
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: boundary",
            "type": "object"
          },
          {
            "description": "Linear brightness shift and contrast slope around mid-gray. Both in [-1, 1], 0 = no change.",
            "properties": {
              "brightness": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "maximum": 1.0,
                    "minimum": -1.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "contrast": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "maximum": 1.0,
                    "minimum": -1.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "brightness-contrast",
                "description": "Selects the `brightness-contrast` operation."
              }
            },
            "required": [
              "op",
              "input"
            ],
            "title": "op: brightness-contrast",
            "type": "object"
          },
          {
            "description": "Brush source. `src` is an `@asset` ref or a literal path/name resolved by the host's AssetLoader.",
            "properties": {
              "op": {
                "const": "brush-file",
                "description": "Selects the `brush-file` operation."
              },
              "src": {
                "description": "Asset reference (`@name`) or literal path.",
                "type": "string"
              }
            },
            "required": [
              "op",
              "src"
            ],
            "title": "op: brush-file",
            "type": "object"
          },
          {
            "description": "Synthesize a crisp constant-width hokusai brush. No scatter / no jitter — dabs are stacked densely so the stroke reads as a solid line.",
            "properties": {
              "aa": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "dabs-per-radius": {
                "oneOf": [
                  {
                    "description": "Dab density along the stroke. Higher = smoother but slower. Default 4.",
                    "minimum": 0.5,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "hardness": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "op": {
                "const": "brush-solid",
                "description": "Selects the `brush-solid` operation."
              },
              "width-px": {
                "oneOf": [
                  {
                    "description": "Stroke width in canvas pixels.",
                    "minimum": 0.4,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "width-px",
              "color"
            ],
            "title": "op: brush-solid",
            "type": "object"
          },
          {
            "description": "Minkowski sum / offset with a disk. Polygons accept negative distance for erosion; lines and points are inflated by |distance|.",
            "properties": {
              "distance": {
                "oneOf": [
                  {
                    "description": "Offset distance in tile pixels. Positive inflates, negative erodes (polygons only).",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "join": {
                "default": "miter",
                "enum": [
                  "miter",
                  "round",
                  "bevel"
                ],
                "type": "string"
              },
              "op": {
                "const": "buffer",
                "description": "Selects the `buffer` operation."
              }
            },
            "required": [
              "op",
              "features",
              "distance"
            ],
            "title": "op: buffer",
            "type": "object"
          },
          {
            "description": "Replace input geometry with one point per polygon / polyline centroid.",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "include-lines": {
                "default": true,
                "type": "boolean"
              },
              "include-polygons": {
                "default": true,
                "type": "boolean"
              },
              "op": {
                "const": "centroid",
                "description": "Selects the `centroid` operation."
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: centroid",
            "type": "object"
          },
          {
            "description": "Rearrange RGBA channels. Each output (`r`/`g`/`b`/`a`) names which input channel feeds it, or a constant (`0`/`1`). Identity by default. Operates in non-premultiplied sRGB.",
            "properties": {
              "a": {
                "enum": [
                  "r",
                  "g",
                  "b",
                  "a",
                  "0",
                  "1"
                ],
                "type": "string"
              },
              "b": {
                "enum": [
                  "r",
                  "g",
                  "b",
                  "a",
                  "0",
                  "1"
                ],
                "type": "string"
              },
              "g": {
                "enum": [
                  "r",
                  "g",
                  "b",
                  "a",
                  "0",
                  "1"
                ],
                "type": "string"
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "channel-shuffle",
                "description": "Selects the `channel-shuffle` operation."
              },
              "r": {
                "enum": [
                  "r",
                  "g",
                  "b",
                  "a",
                  "0",
                  "1"
                ],
                "type": "string"
              }
            },
            "required": [
              "op",
              "input"
            ],
            "title": "op: channel-shuffle",
            "type": "object"
          },
          {
            "description": "Centred disk source. `kind: raster` (default) renders at canvas size with radius = `radius-frac × tile-size`. `kind: sprite` renders at `width-px × height-px` with radius = `radius-frac × min(width, height)`.",
            "properties": {
              "color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "hardness": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "height-px": {
                "minimum": 1,
                "type": "integer"
              },
              "kind": {
                "default": "raster",
                "enum": [
                  "raster",
                  "sprite"
                ],
                "type": "string"
              },
              "op": {
                "const": "circle",
                "description": "Selects the `circle` operation."
              },
              "radius-frac": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "width-px": {
                "minimum": 1,
                "type": "integer"
              }
            },
            "required": [
              "op",
              "color",
              "radius-frac"
            ],
            "title": "op: circle",
            "type": "object"
          },
          {
            "description": "Filled disks at every feature point (tiny-skia), with per-feature radius, color, opacity, and optional stroke ring. The crisp vector counterpart to MapLibre's `circle` layer. Each paint property has an optional `*-expr` MapLibre-expression sibling evaluated per feature group.",
            "properties": {
              "color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "color-expr": {
                "description": "A MapLibre color expression (JSON array, e.g. [\"match\", [\"get\", \"c\"], \"a\", \"#f00\", \"#0f0\"]), evaluated per feature group; overrides the constant `color`."
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "circles",
                "description": "Selects the `circles` operation."
              },
              "opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "opacity-expr": {
                "description": "A MapLibre number expression giving opacity, evaluated per feature group; multiplies both fill and stroke alpha. Overrides the constant `opacity`."
              },
              "radius": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "radius-expr": {
                "description": "A MapLibre number expression (px), evaluated per feature group; overrides the constant `radius`."
              },
              "stroke-color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "stroke-color-expr": {
                "description": "A MapLibre color expression for the stroke ring, evaluated per feature group; overrides the constant `stroke-color`."
              },
              "stroke-width": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "stroke-width-expr": {
                "description": "A MapLibre number expression (px) for the stroke ring, evaluated per feature group; overrides the constant `stroke-width`. A ring is drawn only when width > 0."
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: circles",
            "type": "object"
          },
          {
            "description": "Map a `ScalarField` (or a `Raster`, by its luminance — a gradient map) to colour through a stop table, interpolating between stops in `space`. Samples outside `[stops[0].value, stops[-1].value]` clamp to the end colours. Canonical use case is hypsometric tinting over a DEM (`stops[i].value` = elevation in metres). Give `ramp-expr` instead of `stops` to drive the ramp from a MapLibre color expression over `heatmap-density`.",
            "properties": {
              "field": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "color-ramp",
                "description": "Selects the `color-ramp` operation."
              },
              "opacity": {
                "oneOf": [
                  {
                    "description": "Uniform output-alpha multiplier (layer opacity). Default 1.0. Feed an `expr` node via `@node` for a zoom curve.",
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "ramp-expr": {
                "description": "A MapLibre color expression over `heatmap-density` (e.g. a `heatmap-color` value). When present it overrides `stops`: the expression is baked into a 256-entry LUT per eval (zoom in context), inputs clamp to [0, 1], and lookups interpolate linearly between entries — mirroring MapLibre's 256-px ramp texture. `stops` is only required when this is absent."
              },
              "space": {
                "default": "rgb",
                "description": "Colour space the stops interpolate in. `rgb` (default) is a straight sRGB lerp; `hsl`/`hsv`/`hcl` interpolate hue on the shortest path; `hcl`/`lab` are perceptual (ported from the MapLibre style spec).",
                "enum": [
                  "rgb",
                  "hsl",
                  "hsv",
                  "hcl",
                  "lab"
                ],
                "type": "string"
              },
              "stops": {
                "description": "Either half of a stop may be a `$param`, so a ramp can be recoloured or rescaled at render time. The table is sorted by `value` on every eval, so a param may reorder it.",
                "items": {
                  "properties": {
                    "color": {
                      "description": "sRGB hex color or `$param` reference.",
                      "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|\\$[A-Za-z_][A-Za-z0-9_-]*)$",
                      "type": "string"
                    },
                    "value": {
                      "oneOf": [
                        {
                          "description": "Scalar value at this stop (e.g. metres of elevation for a DEM field).",
                          "type": "number"
                        },
                        {
                          "description": "`$param` reference.",
                          "pattern": "^\\$[A-Za-z_][A-Za-z0-9_-]*$",
                          "type": "string"
                        }
                      ]
                    }
                  },
                  "required": [
                    "value",
                    "color"
                  ],
                  "type": "object"
                },
                "minItems": 2,
                "type": "array"
              }
            },
            "required": [
              "op",
              "field"
            ],
            "title": "op: color-ramp",
            "type": "object"
          },
          {
            "description": "Make pixels close to `color` transparent. Distance ≤ `threshold` → alpha 0, distance ≥ `threshold + softness` → alpha unchanged, linear in between. Distance is Chebyshev (max per-channel) in non-premultiplied sRGB.",
            "properties": {
              "color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "color-to-alpha",
                "description": "Selects the `color-to-alpha` operation."
              },
              "softness": {
                "oneOf": [
                  {
                    "default": 0.1,
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "threshold": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "input",
              "color"
            ],
            "title": "op: color-to-alpha",
            "type": "object"
          },
          {
            "description": "Isolines from a ScalarField (marching squares with linear sub-cell interpolation), chained into polylines. One feature group per level with properties `{\"level\": <number>}`, so data-driven paint can style by level. Levels are `base + k·interval`, or the explicit `levels` array (which overrides both), clamped by `min`/`max`. Chain `dem → contour → stroke` for elevation contours, or `density → contour → stroke` for point-density isolines.",
            "properties": {
              "base": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "description": "Offset the `interval` grid: levels sit at `base + k·interval`.",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "field": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "interval": {
                "oneOf": [
                  {
                    "description": "Spacing between levels, in field units. Required unless `levels` is given.",
                    "exclusiveMinimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "levels": {
                "description": "Explicit levels to extract; overrides `interval`/`base`. A level may be a `$param`; the list is sorted and de-duplicated per render.",
                "items": {
                  "oneOf": [
                    {
                      "type": "number"
                    },
                    {
                      "description": "`$param` reference.",
                      "pattern": "^\\$[A-Za-z_][A-Za-z0-9_-]*$",
                      "type": "string"
                    }
                  ]
                },
                "minItems": 1,
                "type": "array"
              },
              "max": {
                "description": "Emit no level above this value.",
                "type": "number"
              },
              "min": {
                "description": "Emit no level below this value.",
                "type": "number"
              },
              "op": {
                "const": "contour",
                "description": "Selects the `contour` operation."
              }
            },
            "required": [
              "op",
              "field"
            ],
            "title": "op: contour",
            "type": "object"
          },
          {
            "description": "Convex hull of every input vertex as a single polygon.",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "convex-hull",
                "description": "Selects the `convex-hull` operation."
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: convex-hull",
            "type": "object"
          },
          {
            "description": "Cut polylines into dash/gap segments. Lengths are in canvas pixels (consistent with `*-px` brush parameters). Polygons and points pass through unchanged.",
            "properties": {
              "dash-px": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "gap-px": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "op": {
                "const": "dash",
                "description": "Selects the `dash` operation."
              },
              "phase-px": {
                "oneOf": [
                  {
                    "description": "Initial offset into the dash/gap pattern, in pixels. May be negative.",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "features",
              "dash-px",
              "gap-px"
            ],
            "title": "op: dash",
            "type": "object"
          },
          {
            "description": "Sample a host-bound raster DEM as a ScalarField. `source` names a `dem` entry in the document's `sources` block; optional when the document declares exactly one such source.",
            "properties": {
              "op": {
                "const": "dem",
                "description": "Selects the `dem` operation."
              },
              "source": {
                "description": "Name of a `dem` source in the document's `sources` block. Optional when there is exactly one.",
                "type": "string"
              }
            },
            "required": [
              "op"
            ],
            "title": "op: dem",
            "type": "object"
          },
          {
            "description": "Insert intermediate vertices so no segment of any polyline or polygon ring exceeds `target-px`. Original vertices are preserved.",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "densify",
                "description": "Selects the `densify` operation."
              },
              "target-px": {
                "minimum": 0.0,
                "type": "number"
              }
            },
            "required": [
              "op",
              "features",
              "target-px"
            ],
            "title": "op: densify",
            "type": "object"
          },
          {
            "description": "Kernel-density estimate of the input's point features as a ScalarField (the MapLibre heatmap kernel: support = `radius` px, falloff `exp(-0.5·3²·(d/radius)²)/√(2π)`). **Points only** — lines and polygons contribute nothing, so a line or polygon layer comes out an all-zero field with no error; put `centroid` in front of it. Buffer points outside the tile still contribute, so tile borders match. Output is raw accumulated density, unclamped and scaling with `intensity`, so probe its range before placing ramp stops. Map it with `color-ramp` or extract isolines with `contour`. Grows upstream pad by `radius`.",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "intensity": {
                "oneOf": [
                  {
                    "default": 1,
                    "description": "Global multiplier on every point's contribution.",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "intensity-expr": {
                "description": "A MapLibre number expression giving the intensity multiplier, evaluated per feature group; overrides the constant `intensity`."
              },
              "op": {
                "const": "density",
                "description": "Selects the `density` operation."
              },
              "radius": {
                "oneOf": [
                  {
                    "default": 30,
                    "description": "Kernel radius in px. Pad is computed at build time, so this needs a static bound: a literal, or a `$param` with `max`.",
                    "minimum": 0.0,
                    "radius-max": {
                      "description": "Upper bound on `radius` for padding, required when `radius` is an `@node` port. Values above it are clamped.",
                      "minimum": 0.0,
                      "type": "number"
                    },
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "radius-expr": {
                "description": "A MapLibre number expression (px), evaluated per feature group; overrides the constant `radius`. Evaluated values are clamped to `radius`'s static bound so the pad stays sound."
              },
              "weight-expr": {
                "description": "A MapLibre number expression giving a point's weight, evaluated per feature group. A group without it (or whose expression doesn't resolve to a number) weighs 1."
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: density",
            "type": "object"
          },
          {
            "description": "Per-channel morphological max over a square kernel. Grows bright / opaque regions; pair with `erode` to clean up speckle noise (open / close). Being a max, it also flattens detail finer than the kernel — a few px of `radius-px` is enough to erase grain, so add grain *after* morphology, never before. Separable box implementation; grows upstream pad by `radius-px`.",
            "properties": {
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "dilate",
                "description": "Selects the `dilate` operation."
              },
              "radius-px": {
                "oneOf": [
                  {
                    "maximum": 256,
                    "minimum": 0,
                    "type": "integer"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "radius-px-max": {
                "description": "Upper bound on `radius-px` for padding, required when `radius-px` is an `@node` port. Values above it are clamped.",
                "minimum": 0.0,
                "type": "number"
              }
            },
            "required": [
              "op",
              "input",
              "radius-px"
            ],
            "title": "op: dilate",
            "type": "object"
          },
          {
            "description": "Photoshop-style displacement map. Output pixel reads `input` at a position offset by the `displacement` raster's R/G channels (0.5 means no offset). Grows upstream pad by `amp-px` so warped samples stay seamless across tile borders, provided the displacement source is itself seamless (e.g. `noise` with `anchor: world`).",
            "properties": {
              "amp-px": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "amp-x-px": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "amp-x-px-max": {
                "description": "Upper bound on `amp-x-px` for padding, required when `amp-x-px` is an `@node` port. Values above it are clamped.",
                "minimum": 0.0,
                "type": "number"
              },
              "amp-y-px": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "amp-y-px-max": {
                "description": "Upper bound on `amp-y-px` for padding, required when `amp-y-px` is an `@node` port. Values above it are clamped.",
                "minimum": 0.0,
                "type": "number"
              },
              "boundary": {
                "default": "clamp",
                "enum": [
                  "clamp",
                  "transparent",
                  "mirror"
                ],
                "type": "string"
              },
              "displacement": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "displace",
                "description": "Selects the `displace` operation."
              }
            },
            "required": [
              "op",
              "input",
              "displacement",
              "amp-px"
            ],
            "title": "op: displace",
            "type": "object"
          },
          {
            "description": "Reduce to a fixed `palette` with dithering so gradients survive a small palette. `method: \"floyd-steinberg\"` (default) diffuses error to neighbours; `method: \"ordered\"` uses a 4×4 Bayer matrix with strength `amount` (0..1). Nearest-colour distance uses `space` (`lab` default / `rgb`). Alpha preserved.",
            "properties": {
              "amount": {
                "default": 0.5,
                "description": "Ordered-dither strength (ignored for floyd-steinberg).",
                "maximum": 1.0,
                "minimum": 0.0,
                "type": "number"
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "method": {
                "default": "floyd-steinberg",
                "enum": [
                  "floyd-steinberg",
                  "ordered"
                ],
                "type": "string"
              },
              "op": {
                "const": "dither",
                "description": "Selects the `dither` operation."
              },
              "palette": {
                "description": "Each entry is `#rrggbb` / `#rrggbbaa` (alpha ignored for matching) or a `$param`, so a palette can be recoloured at render time.",
                "items": {
                  "description": "sRGB hex color or `$param` reference.",
                  "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|\\$[A-Za-z_][A-Za-z0-9_-]*)$",
                  "type": "string"
                },
                "minItems": 1,
                "type": "array"
              },
              "space": {
                "default": "lab",
                "enum": [
                  "lab",
                  "rgb"
                ],
                "type": "string"
              }
            },
            "required": [
              "op",
              "input",
              "palette"
            ],
            "title": "op: dither",
            "type": "object"
          },
          {
            "description": "Scatter points inside polygons at a per-feature areal density (dot density map).",
            "properties": {
              "density": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "description": "Constant density in units per square kilometre, used where `density-expr` is absent.",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "density-expr": {
                "description": "Per-feature MapLibre expression giving density in units per square kilometre, e.g. [\"/\", [\"get\", \"POP\"], [\"get\", \"AREA_KM2\"]]."
              },
              "dot-value": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "description": "Units one dot stands for. 100 means one dot per 100 people.",
                    "exclusiveMinimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "jitter": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "description": "How far a dot may stray from its cell centre, as a fraction of the cell. 0 leaves a visible grid.",
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "op": {
                "const": "dot-density",
                "description": "Selects the `dot-density` operation."
              },
              "seed": {
                "description": "Lattice salt; change it to reshuffle the dots without changing their density.",
                "minimum": 0,
                "type": "integer"
              },
              "spacing-px": {
                "oneOf": [
                  {
                    "default": 3.0,
                    "description": "Lattice cell size in screen pixels. One dot per cell at most, so this caps how dense the scatter can get.",
                    "exclusiveMinimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: dot-density",
            "type": "object"
          },
          {
            "description": "Sobel gradient magnitude per channel, scaled by `strength` and clamped to [0, 255]. Pass-through over Raster / Sprite. Use after a hillshade for stylised outlines, or on a flat fill to derive an outline mask.",
            "properties": {
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "edge-detect",
                "description": "Selects the `edge-detect` operation."
              },
              "strength": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "description": "Scale factor applied to the gradient magnitude before clamping.",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "input"
            ],
            "title": "op: edge-detect",
            "type": "object"
          },
          {
            "description": "Per-channel morphological min over a square kernel. Shrinks bright / opaque regions; classic mask cleanup after `color-to-alpha`. Removes anything thinner than the kernel outright, so `erode` then `dilate` (an opening) keeps only features wider than `radius-px` — a way to select by size when the source carries no size attribute. Detail finer than the radius does not survive either op: add grain *after* morphology, never before. Separable box implementation; grows upstream pad by `radius-px`.",
            "properties": {
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "erode",
                "description": "Selects the `erode` operation."
              },
              "radius-px": {
                "oneOf": [
                  {
                    "maximum": 256,
                    "minimum": 0,
                    "type": "integer"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "radius-px-max": {
                "description": "Upper bound on `radius-px` for padding, required when `radius-px` is an `@node` port. Values above it are clamped.",
                "minimum": 0.0,
                "type": "number"
              }
            },
            "required": [
              "op",
              "input",
              "radius-px"
            ],
            "title": "op: erode",
            "type": "object"
          },
          {
            "description": "Evaluate a MapLibre expression once per tile (the tile's zoom is in the context) and emit the result as a Scalar — feed it to any node's scalar field via `@node`. Use for zoom curves (`interpolate`/`step`/legacy `{stops}`); per-feature expressions belong on the consuming node's `*-expr` fields instead.",
            "properties": {
              "expr": {
                "description": "A raw MapLibre expression producing `type` (e.g. [\"interpolate\", [\"linear\"], [\"zoom\"], 13, 1, 15, 0])."
              },
              "op": {
                "const": "expr",
                "description": "Selects the `expr` operation."
              },
              "type": {
                "default": "number",
                "description": "The scalar type the expression must produce (build-time typechecked).",
                "enum": [
                  "number",
                  "color",
                  "bool"
                ],
                "type": "string"
              }
            },
            "required": [
              "op",
              "expr"
            ],
            "title": "op: expr",
            "type": "object"
          },
          {
            "description": "Polygon boolean operation between two Features inputs. Subject = `a`, clip = `b`. Lines and points on either input are dropped — booleans are defined on polygons only.",
            "properties": {
              "a": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "b": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "feature-boolean",
                "description": "Selects the `feature-boolean` operation."
              }
            },
            "required": [
              "op",
              "a",
              "b"
            ],
            "title": "op: feature-boolean",
            "type": "object"
          },
          {
            "description": "Sample features from a host-bound feature layer. `source` names an `mvt`/`pmtiles`/`geojson` entry in the document's `sources` block (optional when exactly one exists); `layer` selects a layer within that source (for `geojson`, the layer name equals the source name).",
            "properties": {
              "filter-expr": {
                "description": "A MapLibre filter expression (JSON array, e.g. [\"all\", [\"==\", [\"get\", \"class\"], \"primary\"], [\"has\", \"name\"]]), evaluated per feature. A feature passes only when the expression is truthy. Supports the full expression language (any/has/comparisons/geometry-type)."
              },
              "layer": {
                "description": "Vector tile layer name within `source` (e.g. `earth`, `roads`).",
                "type": "string"
              },
              "max-zoom": {
                "description": "Style-level maximum zoom. Above this zoom the node emits an empty layer.",
                "maximum": 24,
                "minimum": 0,
                "type": "integer"
              },
              "min-zoom": {
                "description": "Style-level minimum zoom. Below this zoom the node emits an empty layer (the asset is not even loaded).",
                "maximum": 24,
                "minimum": 0,
                "type": "integer"
              },
              "min-zoom-field": {
                "description": "Per-feature property name carrying its data-side `min_zoom`. Features with `<field> > z` are dropped.",
                "type": "string"
              },
              "op": {
                "const": "features",
                "description": "Selects the `features` operation."
              },
              "source": {
                "description": "Name of an `mvt`, `pmtiles`, or `geojson` entry in the document's `sources`. Optional — defaults to the only such source when the document declares exactly one.",
                "type": "string"
              }
            },
            "required": [
              "op",
              "layer"
            ],
            "title": "op: features",
            "type": "object"
          },
          {
            "description": "Watercolor scatter-dab fill with world-deterministic jitter (seamless across tiles).",
            "properties": {
              "color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "hardness": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "op": {
                "const": "fill-dabs",
                "description": "Selects the `fill-dabs` operation."
              },
              "opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "opacity-jitter": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "paint": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "position-jitter": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "radius-px": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "size-jitter": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "spacing-px": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "value-jitter": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "features",
              "color",
              "opacity",
              "radius-px",
              "spacing-px"
            ],
            "title": "op: fill-dabs",
            "type": "object"
          },
          {
            "description": "Solid polygon fill with optional outline and Gaussian blur.",
            "properties": {
              "blur-sigma": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "blur-sigma-max": {
                "description": "Upper bound on `blur-sigma` for padding, required when `blur-sigma` is an `@node` port. Values above it are clamped.",
                "minimum": 0.0,
                "type": "number"
              },
              "edge": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "edge-width": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "fill": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "fill-alpha": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "fill-expr": {
                "description": "A MapLibre color expression (JSON array, e.g. [\"match\", [\"get\", \"class\"], \"water\", \"#88c\", \"#ccc\"] or [\"interpolate\", [\"linear\"], [\"get\", \"area\"], 0, \"#eef\", 1000, \"#049\"]), evaluated per feature group at paint time. When present it overrides the constant `fill`; a group whose expression doesn't resolve to a color is not painted. Unlocks continuous data-driven fills that constant `fill` can't express."
              },
              "op": {
                "const": "fill-solid",
                "description": "Selects the `fill-solid` operation."
              },
              "opacity-expr": {
                "description": "A MapLibre number expression (JSON array) giving fill opacity, evaluated per feature group at paint time. When present it overrides the constant `fill-alpha`; a group whose expression doesn't resolve to a number falls back to `fill-alpha`."
              }
            },
            "required": [
              "op",
              "features",
              "fill"
            ],
            "title": "op: fill-solid",
            "type": "object"
          },
          {
            "description": "Conic (sweep) gradient around `center`. Gradient parameter is the clockwise angle from `start-angle` (degrees) normalized to [0, 1). 0° points along +x. `kind: sprite` switches to sprite-local [0, 1] coords.",
            "properties": {
              "anchor": {
                "default": "tile",
                "enum": [
                  "tile",
                  "world"
                ],
                "type": "string"
              },
              "center": {
                "default": [
                  0.5,
                  0.5
                ],
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "height-px": {
                "minimum": 1,
                "type": "integer"
              },
              "kind": {
                "default": "raster",
                "enum": [
                  "raster",
                  "sprite"
                ],
                "type": "string"
              },
              "op": {
                "const": "gradient-conic",
                "description": "Selects the `gradient-conic` operation."
              },
              "space": {
                "default": "rgb",
                "description": "Colour space the stops interpolate in; hue-based spaces take the shortest path.",
                "enum": [
                  "rgb",
                  "hsl",
                  "hsv",
                  "hcl",
                  "lab"
                ],
                "type": "string"
              },
              "start-angle": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "stops": {
                "description": "`[[t, color], ...]`. Either half of a pair may be a `$param`; the table is sorted by `t` on every eval, so stops need not be declared in order.",
                "items": {
                  "items": [
                    {
                      "oneOf": [
                        {
                          "type": "number"
                        },
                        {
                          "description": "`$param` reference.",
                          "pattern": "^\\$[A-Za-z_][A-Za-z0-9_-]*$",
                          "type": "string"
                        }
                      ]
                    },
                    {
                      "description": "sRGB hex color or `$param` reference.",
                      "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|\\$[A-Za-z_][A-Za-z0-9_-]*)$",
                      "type": "string"
                    }
                  ],
                  "maxItems": 2,
                  "minItems": 2,
                  "type": "array"
                },
                "minItems": 2,
                "type": "array"
              },
              "width-px": {
                "minimum": 1,
                "type": "integer"
              }
            },
            "required": [
              "op",
              "stops"
            ],
            "title": "op: gradient-conic",
            "type": "object"
          },
          {
            "description": "Diamond gradient from `center`. Gradient parameter is Manhattan (|dx| + |dy|) distance / radius. `kind: sprite` switches to sprite-local [0, 1] coords.",
            "properties": {
              "anchor": {
                "default": "tile",
                "enum": [
                  "tile",
                  "world"
                ],
                "type": "string"
              },
              "center": {
                "default": [
                  0.5,
                  0.5
                ],
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "height-px": {
                "minimum": 1,
                "type": "integer"
              },
              "kind": {
                "default": "raster",
                "enum": [
                  "raster",
                  "sprite"
                ],
                "type": "string"
              },
              "op": {
                "const": "gradient-diamond",
                "description": "Selects the `gradient-diamond` operation."
              },
              "radius": {
                "oneOf": [
                  {
                    "default": 0.5,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "space": {
                "default": "rgb",
                "description": "Colour space the stops interpolate in; hue-based spaces take the shortest path.",
                "enum": [
                  "rgb",
                  "hsl",
                  "hsv",
                  "hcl",
                  "lab"
                ],
                "type": "string"
              },
              "stops": {
                "description": "`[[t, color], ...]`. Either half of a pair may be a `$param`; the table is sorted by `t` on every eval, so stops need not be declared in order.",
                "items": {
                  "items": [
                    {
                      "oneOf": [
                        {
                          "type": "number"
                        },
                        {
                          "description": "`$param` reference.",
                          "pattern": "^\\$[A-Za-z_][A-Za-z0-9_-]*$",
                          "type": "string"
                        }
                      ]
                    },
                    {
                      "description": "sRGB hex color or `$param` reference.",
                      "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|\\$[A-Za-z_][A-Za-z0-9_-]*)$",
                      "type": "string"
                    }
                  ],
                  "maxItems": 2,
                  "minItems": 2,
                  "type": "array"
                },
                "minItems": 2,
                "type": "array"
              },
              "width-px": {
                "minimum": 1,
                "type": "integer"
              }
            },
            "required": [
              "op",
              "stops"
            ],
            "title": "op: gradient-diamond",
            "type": "object"
          },
          {
            "description": "Linear gradient from `start` to `end` (fractional coords). `kind: raster` (default) samples in tile/world coords; `kind: sprite` samples in [0, 1] sprite coords at `width-px × height-px`.",
            "properties": {
              "anchor": {
                "default": "tile",
                "enum": [
                  "tile",
                  "world"
                ],
                "type": "string"
              },
              "end": {
                "default": [
                  1.0,
                  0.0
                ],
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "height-px": {
                "minimum": 1,
                "type": "integer"
              },
              "kind": {
                "default": "raster",
                "enum": [
                  "raster",
                  "sprite"
                ],
                "type": "string"
              },
              "op": {
                "const": "gradient-linear",
                "description": "Selects the `gradient-linear` operation."
              },
              "space": {
                "default": "rgb",
                "description": "Colour space the stops interpolate in; hue-based spaces take the shortest path.",
                "enum": [
                  "rgb",
                  "hsl",
                  "hsv",
                  "hcl",
                  "lab"
                ],
                "type": "string"
              },
              "start": {
                "default": [
                  0.0,
                  0.0
                ],
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "stops": {
                "description": "`[[t, color], ...]`. Either half of a pair may be a `$param`; the table is sorted by `t` on every eval, so stops need not be declared in order.",
                "items": {
                  "items": [
                    {
                      "oneOf": [
                        {
                          "type": "number"
                        },
                        {
                          "description": "`$param` reference.",
                          "pattern": "^\\$[A-Za-z_][A-Za-z0-9_-]*$",
                          "type": "string"
                        }
                      ]
                    },
                    {
                      "description": "sRGB hex color or `$param` reference.",
                      "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|\\$[A-Za-z_][A-Za-z0-9_-]*)$",
                      "type": "string"
                    }
                  ],
                  "maxItems": 2,
                  "minItems": 2,
                  "type": "array"
                },
                "minItems": 2,
                "type": "array"
              },
              "width-px": {
                "minimum": 1,
                "type": "integer"
              }
            },
            "required": [
              "op",
              "stops"
            ],
            "title": "op: gradient-linear",
            "type": "object"
          },
          {
            "description": "Radial gradient from `center` outward. Gradient parameter is Euclidean distance / radius. `aspect > 1` stretches the ellipse along X, `< 1` along Y. `kind: sprite` switches to sprite-local [0, 1] coords.",
            "properties": {
              "anchor": {
                "default": "tile",
                "enum": [
                  "tile",
                  "world"
                ],
                "type": "string"
              },
              "aspect": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "center": {
                "default": [
                  0.5,
                  0.5
                ],
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "height-px": {
                "minimum": 1,
                "type": "integer"
              },
              "kind": {
                "default": "raster",
                "enum": [
                  "raster",
                  "sprite"
                ],
                "type": "string"
              },
              "op": {
                "const": "gradient-radial",
                "description": "Selects the `gradient-radial` operation."
              },
              "radius": {
                "oneOf": [
                  {
                    "default": 0.5,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "space": {
                "default": "rgb",
                "description": "Colour space the stops interpolate in; hue-based spaces take the shortest path.",
                "enum": [
                  "rgb",
                  "hsl",
                  "hsv",
                  "hcl",
                  "lab"
                ],
                "type": "string"
              },
              "stops": {
                "description": "`[[t, color], ...]`. Either half of a pair may be a `$param`; the table is sorted by `t` on every eval, so stops need not be declared in order.",
                "items": {
                  "items": [
                    {
                      "oneOf": [
                        {
                          "type": "number"
                        },
                        {
                          "description": "`$param` reference.",
                          "pattern": "^\\$[A-Za-z_][A-Za-z0-9_-]*$",
                          "type": "string"
                        }
                      ]
                    },
                    {
                      "description": "sRGB hex color or `$param` reference.",
                      "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|\\$[A-Za-z_][A-Za-z0-9_-]*)$",
                      "type": "string"
                    }
                  ],
                  "maxItems": 2,
                  "minItems": 2,
                  "type": "array"
                },
                "minItems": 2,
                "type": "array"
              },
              "width-px": {
                "minimum": 1,
                "type": "integer"
              }
            },
            "required": [
              "op",
              "stops"
            ],
            "title": "op: gradient-radial",
            "type": "object"
          },
          {
            "description": "Parallels and meridians crossing the tile, as labelled polylines.",
            "properties": {
              "axes": {
                "default": "both",
                "description": "Which families to emit: parallels of latitude, meridians of longitude, or both.",
                "enum": [
                  "both",
                  "parallels",
                  "meridians"
                ],
                "type": "string"
              },
              "extent": {
                "default": 4096,
                "description": "Coordinate extent of the emitted geometry, matching the features it is drawn alongside.",
                "minimum": 1,
                "type": "integer"
              },
              "interval-deg": {
                "oneOf": [
                  {
                    "description": "Spacing in degrees. Omit to pick one from the zoom along the 30/10/5/2/1 ladder.",
                    "exclusiveMinimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "op": {
                "const": "graticule",
                "description": "Selects the `graticule` operation."
              }
            },
            "required": [
              "op"
            ],
            "title": "op: graticule",
            "type": "object"
          },
          {
            "description": "Parallel-line hatching of input polygons.",
            "properties": {
              "angle-deg": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "description": "Hatch direction (degrees, CCW from +X).",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "hatch",
                "description": "Selects the `hatch` operation."
              },
              "phase": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "description": "Per-line offset in spacing units (0..1 cycles through one period).",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "spacing": {
                "oneOf": [
                  {
                    "description": "Perpendicular spacing between consecutive lines, in tile pixels.",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "features",
              "spacing"
            ],
            "title": "op: hatch",
            "type": "object"
          },
          {
            "description": "Analytical hillshade (Horn 1981) from a ScalarField. `mode: shade` outputs grayscale; `mode: relief` outputs transparent black scaled by 1-shade for multiply-blend over a base map.",
            "properties": {
              "altitude-deg": {
                "oneOf": [
                  {
                    "default": 45,
                    "description": "Light elevation above the horizon (degrees).",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "azimuth-deg": {
                "oneOf": [
                  {
                    "default": 315,
                    "description": "Light direction (0 = north, clockwise).",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "exaggeration": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "description": "Extra vertical exaggeration on top of z-factor.",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "field": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "highlight-color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "mode": {
                "default": "shade",
                "enum": [
                  "shade",
                  "relief"
                ],
                "type": "string"
              },
              "multidirectional": {
                "default": false,
                "description": "ESRI-style 4-direction weighted hillshade — softer, more legible at small zooms.",
                "oneOf": [
                  {
                    "type": "boolean"
                  },
                  {
                    "pattern": "^[$@].+",
                    "type": "string"
                  }
                ]
              },
              "op": {
                "const": "hillshade",
                "description": "Selects the `hillshade` operation."
              },
              "shadow-color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "z-factor": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "description": "Multiplier on elevation before gradient — use ~1/cos(lat) for high latitudes.",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "field"
            ],
            "title": "op: hillshade",
            "type": "object"
          },
          {
            "description": "HSL adjustment: rotate hue by `hue-shift` degrees, shift saturation/lightness in [-1, 1] (0 = no change, +1 = toward max, -1 = toward 0).",
            "properties": {
              "hue-shift": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "lightness": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "maximum": 1.0,
                    "minimum": -1.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "op": {
                "const": "hsl",
                "description": "Selects the `hsl` operation."
              },
              "saturation": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "maximum": 1.0,
                    "minimum": -1.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "input"
            ],
            "title": "op: hsl",
            "type": "object"
          },
          {
            "description": "Crop one named icon from a `sprite` source's atlas. `sprite` is an `@sprite-source` ref (or literal atlas key); `name` is the icon's index key. Output is a Sprite at the icon's atlas pixel size.",
            "properties": {
              "name": {
                "type": "string"
              },
              "op": {
                "const": "icon",
                "description": "Selects the `icon` operation."
              },
              "sprite": {
                "description": "Asset reference (`@name`) or literal path.",
                "type": "string"
              }
            },
            "required": [
              "op",
              "sprite",
              "name"
            ],
            "title": "op: icon",
            "type": "object"
          },
          {
            "description": "Image asset source. `src` is an `@asset` ref or a literal path/name resolved by the host's AssetLoader. Output preserves the source dimensions.",
            "properties": {
              "op": {
                "const": "image",
                "description": "Selects the `image` operation."
              },
              "src": {
                "description": "Asset reference (`@name`) or literal path.",
                "type": "string"
              }
            },
            "required": [
              "op",
              "src"
            ],
            "title": "op: image",
            "type": "object"
          },
          {
            "description": "Negate RGB channels (1 - c). Alpha is preserved.",
            "properties": {
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "invert",
                "description": "Selects the `invert` operation."
              }
            },
            "required": [
              "op",
              "input"
            ],
            "title": "op: invert",
            "type": "object"
          },
          {
            "description": "Place the labels of every `text-labels` layer against one shared collision index, the way maplibre-gl-js does — so a POI label can knock out an overlapping road name. `labels` lists the layers bottom-first (paint order, as in `stack`); priority is top-down, so the last entry places first and wins. Feed the result to each layer's `text-draw` node.",
            "properties": {
              "labels": {
                "description": "`text-labels` layers, bottom first. The topmost layer places first.",
                "items": {
                  "description": "Reference to another node (`@name`).",
                  "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                  "type": "string"
                },
                "minItems": 1,
                "type": "array"
              },
              "op": {
                "const": "label-placement",
                "description": "Selects the `label-placement` operation."
              }
            },
            "required": [
              "op",
              "labels"
            ],
            "title": "op: label-placement",
            "type": "object"
          },
          {
            "description": "Photoshop-style levels: remap input range [in-black, in-white] through `gamma` onto output range [out-black, out-white]. All values in [0, 1] (non-premultiplied sRGB). `gamma > 1` lightens midtones; `gamma < 1` darkens them.",
            "properties": {
              "gamma": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "minimum": 0.01,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "in-black": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "in-white": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "levels",
                "description": "Selects the `levels` operation."
              },
              "out-black": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "out-white": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "input"
            ],
            "title": "op: levels",
            "type": "object"
          },
          {
            "description": "Brush stroke along MVT polylines.",
            "properties": {
              "brush": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "dtime": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "dtime-stroke-curve": {
                "description": "Piecewise-linear `[[t, y], ...]` multiplier on `dtime`. `y` scales the per-vertex dtime — y=3 makes the brush linger 3× longer (slower hand), y=0.3 sweeps through 3× faster. Used with dynamics-driven brushes that respond to stroke speed.",
                "items": {
                  "items": {
                    "oneOf": [
                      {
                        "type": "number"
                      },
                      {
                        "description": "`$param` reference.",
                        "pattern": "^\\$[A-Za-z_][A-Za-z0-9_-]*$",
                        "type": "string"
                      }
                    ]
                  },
                  "maxItems": 2,
                  "minItems": 2,
                  "type": "array"
                },
                "minItems": 2,
                "type": "array"
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "hardness-stroke-curve": {
                "description": "Piecewise-linear `[[t, y], ...]` driving a libmypaint `stroke` input on the brush. `t` is normalized stroke progress in [0, 1]; `y` is an offset added to the setting's base value. `radius` is log-space (y=-2.3 ≈ ×0.1, y=+0.69 ≈ ×2); `opaque` and `hardness` are linear.",
                "items": {
                  "items": {
                    "oneOf": [
                      {
                        "type": "number"
                      },
                      {
                        "description": "`$param` reference.",
                        "pattern": "^\\$[A-Za-z_][A-Za-z0-9_-]*$",
                        "type": "string"
                      }
                    ]
                  },
                  "maxItems": 2,
                  "minItems": 2,
                  "type": "array"
                },
                "minItems": 2,
                "type": "array"
              },
              "op": {
                "const": "line",
                "description": "Selects the `line` operation."
              },
              "opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "opacity-stroke-curve": {
                "description": "Piecewise-linear `[[t, y], ...]` driving a libmypaint `stroke` input on the brush. `t` is normalized stroke progress in [0, 1]; `y` is an offset added to the setting's base value. `radius` is log-space (y=-2.3 ≈ ×0.1, y=+0.69 ≈ ×2); `opaque` and `hardness` are linear.",
                "items": {
                  "items": {
                    "oneOf": [
                      {
                        "type": "number"
                      },
                      {
                        "description": "`$param` reference.",
                        "pattern": "^\\$[A-Za-z_][A-Za-z0-9_-]*$",
                        "type": "string"
                      }
                    ]
                  },
                  "maxItems": 2,
                  "minItems": 2,
                  "type": "array"
                },
                "minItems": 2,
                "type": "array"
              },
              "pressure-base": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "pressure-jitter": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "radius-px": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "radius-stroke-curve": {
                "description": "Piecewise-linear `[[t, y], ...]` driving a libmypaint `stroke` input on the brush. `t` is normalized stroke progress in [0, 1]; `y` is an offset added to the setting's base value. `radius` is log-space (y=-2.3 ≈ ×0.1, y=+0.69 ≈ ×2); `opaque` and `hardness` are linear.",
                "items": {
                  "items": {
                    "oneOf": [
                      {
                        "type": "number"
                      },
                      {
                        "description": "`$param` reference.",
                        "pattern": "^\\$[A-Za-z_][A-Za-z0-9_-]*$",
                        "type": "string"
                      }
                    ]
                  },
                  "maxItems": 2,
                  "minItems": 2,
                  "type": "array"
                },
                "minItems": 2,
                "type": "array"
              }
            },
            "required": [
              "op",
              "features",
              "brush",
              "color"
            ],
            "title": "op: line",
            "type": "object"
          },
          {
            "description": "Repeat a sprite along each polyline, rotated to the line's tangent (MapLibre `line-pattern`). Points/polygons ignored. `width-px` scales the image height to the stroke width; `spacing-px` sets the advance (0 = the scaled image width, seamless).",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "image": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "line-stamp",
                "description": "Selects the `line-stamp` operation."
              },
              "opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "scale": {
                "oneOf": [
                  {
                    "description": "Extra uniform scale on top of the width fit. Default 1.0.",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "spacing-px": {
                "oneOf": [
                  {
                    "description": "Advance between stamps. 0 = the scaled image width (seamless tiling).",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "width-px": {
                "oneOf": [
                  {
                    "description": "Stroke width; the image height is fit to it. 0 = native height.",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "features",
              "image"
            ],
            "title": "op: line-stamp",
            "type": "object"
          },
          {
            "description": "Literal geometry source. Coordinates are tile-local pixels in [0, extent].",
            "properties": {
              "extent": {
                "default": 4096,
                "minimum": 1,
                "type": "integer"
              },
              "lines": {
                "items": {
                  "items": {
                    "items": {
                      "type": "number"
                    },
                    "maxItems": 2,
                    "minItems": 2,
                    "type": "array"
                  },
                  "type": "array"
                },
                "type": "array"
              },
              "op": {
                "const": "literal-geometry",
                "description": "Selects the `literal-geometry` operation."
              },
              "points": {
                "items": {
                  "items": {
                    "type": "number"
                  },
                  "maxItems": 2,
                  "minItems": 2,
                  "type": "array"
                },
                "type": "array"
              },
              "polygons": {
                "items": {
                  "properties": {
                    "exterior": {
                      "items": {
                        "items": {
                          "type": "number"
                        },
                        "maxItems": 2,
                        "minItems": 2,
                        "type": "array"
                      },
                      "type": "array"
                    },
                    "holes": {
                      "items": {
                        "items": {
                          "items": {
                            "type": "number"
                          },
                          "maxItems": 2,
                          "minItems": 2,
                          "type": "array"
                        },
                        "type": "array"
                      },
                      "type": "array"
                    }
                  },
                  "required": [
                    "exterior"
                  ],
                  "type": "object"
                },
                "type": "array"
              }
            },
            "required": [
              "op"
            ],
            "title": "op: literal-geometry",
            "type": "object"
          },
          {
            "description": "Linearly remap scalar field values from [in-min, in-max] to [out-min, out-max]. With `clamp: true`, results outside the output range are pinned to the range bounds. Useful for normalising elevation or distance fields before a `color-ramp`.",
            "properties": {
              "clamp": {
                "default": false,
                "oneOf": [
                  {
                    "type": "boolean"
                  },
                  {
                    "pattern": "^[$@].+",
                    "type": "string"
                  }
                ]
              },
              "field": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "in-max": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "in-min": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "op": {
                "const": "map-range",
                "description": "Selects the `map-range` operation."
              },
              "out-max": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "out-min": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "field"
            ],
            "title": "op: map-range",
            "type": "object"
          },
          {
            "description": "Arithmetic over scalar numbers. Operands accept literals, `$param` references, and `@node` scalar ports. Unary fns use `a`; binary `a b`; `clamp(a, b=lo, c=hi)`, `lerp(a, b, c=t)`.",
            "properties": {
              "a": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "b": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "c": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "fn": {
                "enum": [
                  "abs",
                  "neg",
                  "floor",
                  "ceil",
                  "round",
                  "sqrt",
                  "add",
                  "sub",
                  "mul",
                  "div",
                  "mod",
                  "min",
                  "max",
                  "pow",
                  "clamp",
                  "lerp"
                ],
                "type": "string"
              },
              "op": {
                "const": "math",
                "description": "Selects the `math` operation."
              }
            },
            "required": [
              "op",
              "fn",
              "a"
            ],
            "title": "op: math",
            "type": "object"
          },
          {
            "description": "Approximate medial axis of each input polygon, emitted as polylines. Smaller `densify-px` → more accurate but slower; `min-branch-px` prunes short side branches after stitching.",
            "properties": {
              "densify-px": {
                "oneOf": [
                  {
                    "default": 4.0,
                    "minimum": 0.5,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "min-branch-px": {
                "oneOf": [
                  {
                    "default": 8.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "op": {
                "const": "medial-axis",
                "description": "Selects the `medial-axis` operation."
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: medial-axis",
            "type": "object"
          },
          {
            "description": "Interpolate two rasters per-pixel by scalar `t` (0..1) in colour `space`. A straight colour tween (contrast with `blend`, which composites with alpha). `t` accepts a literal, `$param`, or `@node` scalar for param-driven theming.",
            "properties": {
              "a": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "b": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "mix",
                "description": "Selects the `mix` operation."
              },
              "space": {
                "default": "rgb",
                "description": "Colour space the interpolation runs in; hue-based spaces take the shortest path.",
                "enum": [
                  "rgb",
                  "hsl",
                  "hsv",
                  "hcl",
                  "lab"
                ],
                "type": "string"
              },
              "t": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "a",
              "b"
            ],
            "title": "op: mix",
            "type": "object"
          },
          {
            "description": "Quantize a raster into uniform square blocks. `mode` selects how each block is summarized: `average` (default) takes the mean of covered pixels (smooth, classic mosaic); `nearest` takes the block's centre pixel (crisp, no inter-colour blending). World-anchored by default so adjacent map tiles share the same block grid.",
            "properties": {
              "anchor": {
                "default": "world",
                "description": "`world` (default) makes the block grid seamless across map tiles by growing the upstream pad. `tile` restarts the grid at every map tile's top-left and requires no extra padding.",
                "enum": [
                  "world",
                  "tile"
                ],
                "type": "string"
              },
              "block": {
                "oneOf": [
                  {
                    "description": "Block edge length in canvas pixels. With `anchor: world` the block size sets the canvas padding, which is fixed before anything renders — so this needs an upper bound the build can see: a literal, a `$param` with `max`, or `block-max` beside an `@node` port.",
                    "minimum": 1,
                    "type": "integer"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "block-max": {
                "description": "Upper bound on `block` for padding, required when `block` is an `@node` port. Values above it are clamped.",
                "minimum": 0.0,
                "type": "number"
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "mode": {
                "default": "average",
                "description": "`average` (default) blends covered pixels into a mean colour. `nearest` samples the block's centre pixel verbatim — produces hard block edges without inter-colour averaging.",
                "enum": [
                  "average",
                  "nearest"
                ],
                "type": "string"
              },
              "op": {
                "const": "mosaic",
                "description": "Selects the `mosaic` operation."
              }
            },
            "required": [
              "op",
              "input",
              "block"
            ],
            "title": "op: mosaic",
            "type": "object"
          },
          {
            "description": "Procedural noise source. With `kind: raster` (default) the noise is mapped to RGBA via `low-color`/`high-color`/`opacity`. With `kind: scalar` it emits a `ScalarField` of raw fBm values — compose with `map-range` before feeding `hillshade`/`color-ramp`. Every `type` lands in [-1, 1], but they are not distributed alike: `white`/`value`/`perlin`/`simplex` sit around 0, while `worley` is `1 − 2·distance` and so piles up *at* +1 over the cell interiors — more than half a tile can be exactly 1.0. Evenly spaced ramp stops over a field like that leave most of the ramp unreachable and the render unchanged, so probe the distribution before choosing stops. `anchor=world` (default) keeps the field seamless across tile borders.",
            "properties": {
              "anchor": {
                "default": "world",
                "enum": [
                  "tile",
                  "world"
                ],
                "type": "string"
              },
              "gain": {
                "oneOf": [
                  {
                    "default": 0.5,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "high-color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "kind": {
                "default": "raster",
                "enum": [
                  "raster",
                  "scalar"
                ],
                "type": "string"
              },
              "lacunarity": {
                "oneOf": [
                  {
                    "default": 2.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "low-color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "octaves": {
                "default": 1,
                "maximum": 12,
                "minimum": 1,
                "type": "integer"
              },
              "op": {
                "const": "noise",
                "description": "Selects the `noise` operation."
              },
              "opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "scale-px": {
                "description": "Noise wavelength in pixels. A single number for isotropic noise; an `[x, y]` array for anisotropic noise (larger value = longer wavelength along that axis, i.e. stretched pattern).",
                "oneOf": [
                  {
                    "exclusiveMinimum": 0,
                    "type": "number"
                  },
                  {
                    "items": {
                      "exclusiveMinimum": 0,
                      "type": "number"
                    },
                    "maxItems": 2,
                    "minItems": 2,
                    "type": "array"
                  }
                ]
              },
              "seed": {
                "description": "Field seed. Omitted, `anchor: world` uses a fixed seed so every tile samples the same field, and `anchor: tile` uses the host's per-tile seed so each tile gets its own. Set it to pin either.",
                "minimum": 0,
                "type": "integer"
              },
              "type": {
                "default": "perlin",
                "enum": [
                  "white",
                  "value",
                  "perlin",
                  "simplex",
                  "worley"
                ],
                "type": "string"
              },
              "warp-amp": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "warp-freq": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "scale-px"
            ],
            "title": "op: noise",
            "type": "object"
          },
          {
            "description": "Extract a single channel of an RGBA raster as a [0, 1] ScalarField. RGB channels are read in non-premultiplied space; `luminance` is Rec. 601 luma over the non-premultiplied colour. Bridges the raster pipeline into scalar-field ops (`map-range`, `threshold`, `color-ramp`).",
            "properties": {
              "channel": {
                "default": "luminance",
                "enum": [
                  "r",
                  "g",
                  "b",
                  "a",
                  "luminance"
                ],
                "type": "string"
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "pick-channel",
                "description": "Selects the `pick-channel` operation."
              }
            },
            "required": [
              "op",
              "input"
            ],
            "title": "op: pick-channel",
            "type": "object"
          },
          {
            "description": "Draw the source raster onto the canvas exactly once. `fit:none` (default) uses position/anchor/scale; `cover`/`contain`/`stretch` derive the destination rect from the canvas (and treat `position-px` as an offset from canvas center). Companion to `tiling` (repeats) and `stamp` (per-point).",
            "properties": {
              "anchor": {
                "default": "top-left",
                "description": "Only used when fit=none.",
                "enum": [
                  "top-left",
                  "top-center",
                  "top-right",
                  "center-left",
                  "center",
                  "center-right",
                  "bottom-left",
                  "bottom-center",
                  "bottom-right"
                ],
                "type": "string"
              },
              "fit": {
                "default": "none",
                "enum": [
                  "none",
                  "cover",
                  "contain",
                  "stretch"
                ],
                "type": "string"
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "place",
                "description": "Selects the `place` operation."
              },
              "opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "position-px": {
                "description": "fit=none: canvas pixel where the image's `anchor` point lands. Other fits: offset from canvas center.",
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "rotation-deg": {
                "oneOf": [
                  {
                    "description": "Rotation (degrees clockwise) around the placement anchor / canvas center.",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "scale": {
                "oneOf": [
                  {
                    "description": "Uniform scale for fit=none. Ignored for other fits.",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "input"
            ],
            "title": "op: place",
            "type": "object"
          },
          {
            "description": "Regular lattice of points covering the tile.",
            "properties": {
              "anchor": {
                "default": "tile",
                "enum": [
                  "tile",
                  "world"
                ],
                "type": "string"
              },
              "extent": {
                "default": 4096,
                "minimum": 1,
                "type": "integer"
              },
              "offset-x": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "offset-y": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "op": {
                "const": "point-grid",
                "description": "Selects the `point-grid` operation."
              },
              "spacing": {
                "oneOf": [
                  {
                    "description": "Grid pitch in *extent* units, not pixels — `extent` below is the coordinate space (4096 by default), so a 512 px tile fits 8 points at `spacing: 512`. Paint ops downstream take pixels, so the two do not match.",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "spacing-x": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "spacing-y": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "spacing"
            ],
            "title": "op: point-grid",
            "type": "object"
          },
          {
            "description": "Random points at a given mean spacing, covering the tile. Where `point-grid` puts exactly one point in the middle of every cell, this one draws a *count* per cell — 0 to 3, averaging 1 — and places each point at a random spot inside it, so the set has the clumps and gaps of a real random scatter and no residual pitch. Reach for `point-grid` when the regularity is the point (a halftone screen, a tiling of panes to fracture) and for `point-scatter` when it is the enemy (stipple, foliage, grain, scattered symbols). Jittering a `point-grid` is not a substitute: one point per cell means an even density, and the lattice frequency stays plainly visible.",
            "properties": {
              "anchor": {
                "default": "world",
                "description": "`world` indexes the cells off global (0, 0), so adjacent tiles agree on the points they share and the scatter is seamless — the usual choice. `tile` indexes off the tile's own corner, giving every tile the same point set and a visible seam.",
                "enum": [
                  "tile",
                  "world"
                ],
                "type": "string"
              },
              "extent": {
                "default": 4096,
                "minimum": 1,
                "type": "integer"
              },
              "op": {
                "const": "point-scatter",
                "description": "Selects the `point-scatter` operation."
              },
              "seed": {
                "description": "Optional explicit u32 seed. Change it to get a different scatter at the same spacing, or to keep two `point-scatter` nodes from landing on each other. Default: a fixed constant, so the pattern is stable across tiles and runs.",
                "minimum": 0,
                "type": "integer"
              },
              "spacing": {
                "oneOf": [
                  {
                    "description": "Mean spacing in *extent* units, not pixels — one point per `spacing` × `spacing` cell on average. `extent` below is the coordinate space (4096 by default), so a 512 px tile averages 8 points across at `spacing: 512`. Paint ops downstream take pixels, so the two do not match.",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "spacing-x": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "spacing-y": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "spacing"
            ],
            "title": "op: point-scatter",
            "type": "object"
          },
          {
            "description": "Quantise each RGB channel into `steps` evenly-spaced levels (in non-premultiplied sRGB). Alpha preserved. Pair with `levels` to control which tonal band gets the strongest banding.",
            "properties": {
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "posterize",
                "description": "Selects the `posterize` operation."
              },
              "steps": {
                "default": 4,
                "maximum": 256,
                "minimum": 2,
                "type": "integer"
              }
            },
            "required": [
              "op",
              "input"
            ],
            "title": "op: posterize",
            "type": "object"
          },
          {
            "description": "Snap each pixel to the nearest colour in `palette`, measuring distance in `space` (`lab` = perceptual ΔE, default; or `rgb`). Alpha is preserved. Use for limited-palette / poster / pixel-art looks; see `dither` for the error-diffused variant.",
            "properties": {
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "quantize",
                "description": "Selects the `quantize` operation."
              },
              "palette": {
                "description": "Each entry is `#rrggbb` / `#rrggbbaa` (alpha ignored for matching) or a `$param`, so a palette can be recoloured at render time.",
                "items": {
                  "description": "sRGB hex color or `$param` reference.",
                  "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|\\$[A-Za-z_][A-Za-z0-9_-]*)$",
                  "type": "string"
                },
                "minItems": 1,
                "type": "array"
              },
              "space": {
                "default": "lab",
                "description": "Colour space the nearest-colour distance is measured in.",
                "enum": [
                  "lab",
                  "rgb"
                ],
                "type": "string"
              }
            },
            "required": [
              "op",
              "input",
              "palette"
            ],
            "title": "op: quantize",
            "type": "object"
          },
          {
            "description": "Sample a host-bound RGBA tile pyramid (satellite imagery, pre-rendered basemaps) as a canvas-padded Raster. `source` names a `raster` entry in the document's `sources` block; optional when the document declares exactly one such source.",
            "properties": {
              "op": {
                "const": "raster",
                "description": "Selects the `raster` operation."
              },
              "source": {
                "description": "Name of a `raster` source in the document's `sources` block. Optional when there is exactly one.",
                "type": "string"
              }
            },
            "required": [
              "op"
            ],
            "title": "op: raster",
            "type": "object"
          },
          {
            "description": "Resample each polyline / polygon ring to evenly-spaced vertices at `spacing-px` along arc length. Original vertices are not preserved.",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "resample",
                "description": "Selects the `resample` operation."
              },
              "spacing-px": {
                "minimum": 0.0,
                "type": "number"
              }
            },
            "required": [
              "op",
              "features",
              "spacing-px"
            ],
            "title": "op: resample",
            "type": "object"
          },
          {
            "description": "Scale CIELAB chroma uniformly by `amount` (1 = identity, 0 = greyscale, >1 = more saturated). Hue and lightness are preserved.",
            "properties": {
              "amount": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "saturate",
                "description": "Selects the `saturate` operation."
              }
            },
            "required": [
              "op",
              "input"
            ],
            "title": "op: saturate",
            "type": "object"
          },
          {
            "description": "4-neighbour Laplacian sharpen: amplifies each pixel relative to its orthogonal neighbours by `amount`. Around 0.5–1.0 is a typical unsharp-mask look; negative values give a soft halo. Grows upstream pad by 1.",
            "properties": {
              "amount": {
                "oneOf": [
                  {
                    "default": 0.5,
                    "description": "Sharpening strength. 0 = pass-through, ~1 = strong.",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "sharpen",
                "description": "Selects the `sharpen` operation."
              }
            },
            "required": [
              "op",
              "input"
            ],
            "title": "op: sharpen",
            "type": "object"
          },
          {
            "description": "Douglas-Peucker simplify polylines and polygon rings; points pass through.",
            "properties": {
              "epsilon": {
                "oneOf": [
                  {
                    "description": "Max perpendicular distance a vertex may be from the simplified line, in tile pixels.",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "simplify",
                "description": "Selects the `simplify` operation."
              }
            },
            "required": [
              "op",
              "features",
              "epsilon"
            ],
            "title": "op: simplify",
            "type": "object"
          },
          {
            "description": "Slope angle as grayscale, normalised to 0..1 against `max-deg`.",
            "properties": {
              "field": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "invert": {
                "default": false,
                "description": "If true, flat = white and steep = black.",
                "oneOf": [
                  {
                    "type": "boolean"
                  },
                  {
                    "pattern": "^[$@].+",
                    "type": "string"
                  }
                ]
              },
              "max-deg": {
                "oneOf": [
                  {
                    "default": 60,
                    "description": "Slope angle (degrees) that maps to white.",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "op": {
                "const": "slope",
                "description": "Selects the `slope` operation."
              }
            },
            "required": [
              "op",
              "field"
            ],
            "title": "op: slope",
            "type": "object"
          },
          {
            "description": "Chaikin corner-smoothing over each input polyline and polygon ring. Each iteration roughly doubles the vertex count; 2–3 iterations are usually enough.",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "iterations": {
                "default": 2,
                "maximum": 8,
                "minimum": 0,
                "type": "integer"
              },
              "op": {
                "const": "smooth",
                "description": "Selects the `smooth` operation."
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: smooth",
            "type": "object"
          },
          {
            "description": "Solid-colour source. `kind: raster` (default) fills the full canvas; `kind: sprite` emits a Sprite at `width-px × height-px` for use as a synthetic placement/tiling source.",
            "properties": {
              "color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "height-px": {
                "minimum": 1,
                "type": "integer"
              },
              "kind": {
                "default": "raster",
                "enum": [
                  "raster",
                  "sprite"
                ],
                "type": "string"
              },
              "op": {
                "const": "solid",
                "description": "Selects the `solid` operation."
              },
              "width-px": {
                "minimum": 1,
                "type": "integer"
              }
            },
            "required": [
              "op",
              "color"
            ],
            "title": "op: solid",
            "type": "object"
          },
          {
            "description": "Composite an ordered list of raster `layers` bottom-to-top with plain source-over (equivalent to a chain of `blend` nodes with `mode: normal`, `composite: over`, `opacity: 1`), folded into one pass over a single accumulator. Fully-transparent layers are skipped. For per-layer opacity, blend modes, clipping, masks, or the eraser composite, use `blend`.",
            "properties": {
              "layers": {
                "description": "Raster (or sprite) layers, bottom first. The output mirrors the bottom layer's kind.",
                "items": {
                  "description": "Reference to another node (`@name`).",
                  "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                  "type": "string"
                },
                "minItems": 1,
                "type": "array"
              },
              "op": {
                "const": "stack",
                "description": "Selects the `stack` operation."
              }
            },
            "required": [
              "op",
              "layers"
            ],
            "title": "op: stack",
            "type": "object"
          },
          {
            "description": "Stamp a sprite at every input point. Lines and polygons are ignored. Rotation, scale and position jitter are world-deterministic — a given point gets the same jitter no matter which tile renders it, and the three do not correlate. Provide either an `image` input (one sprite for every point) or, for a data-driven `icon-image`, a `sprite` atlas ref plus a `name-expr` that names each feature's icon.",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "image": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "name-expr": {
                "description": "A MapLibre expression giving each feature's icon name (MapLibre data-driven `icon-image`), cropped per group from the `sprite` atlas. Used instead of an `image` input; any icon the bound sheet contains is croppable, so no enumeration is needed."
              },
              "op": {
                "const": "stamp",
                "description": "Selects the `stamp` operation."
              },
              "opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "opacity-expr": {
                "description": "A MapLibre number expression giving opacity, evaluated per feature group; overrides the constant `opacity`."
              },
              "position-jitter-px": {
                "oneOf": [
                  {
                    "description": "Per-point random offset, ±value canvas pixels, drawn independently on x and y. Breaks up the regular lattice a `point-grid` leaves. Default 0 (no offset).",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "rotation-deg": {
                "oneOf": [
                  {
                    "description": "Constant rotation around each point, in degrees clockwise.",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "rotation-deg-expr": {
                "description": "A MapLibre number expression (degrees clockwise), evaluated per feature group; overrides the constant `rotation-deg`."
              },
              "rotation-jitter-deg": {
                "oneOf": [
                  {
                    "description": "Per-point random rotation, ±value degrees.",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "scale": {
                "oneOf": [
                  {
                    "description": "Uniform scale applied to the sprite. Default 1.0 (native size).",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "scale-expr": {
                "description": "A MapLibre number expression, evaluated per feature group; overrides the constant `scale`. A group whose expression doesn't resolve to a number falls back to `scale`."
              },
              "scale-jitter": {
                "oneOf": [
                  {
                    "description": "Per-point random scale, ±value as a fraction of `scale` (0.2 = ±20%).",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "sprite": {
                "description": "Asset reference (`@name`) or literal path.",
                "type": "string"
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: stamp",
            "type": "object"
          },
          {
            "description": "Crisp constant-width vector stroke along feature polylines (tiny-skia), with cap/join and optional pixel `dasharray`. The sharp counterpart to `line` (painterly brush) — for clean cartographic road/boundary lines.",
            "properties": {
              "cap": {
                "default": "butt",
                "enum": [
                  "butt",
                  "round",
                  "square"
                ],
                "type": "string"
              },
              "color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "color-expr": {
                "description": "A MapLibre color expression (JSON array, e.g. [\"match\", [\"get\", \"class\"], \"river\", \"#48b\", \"#888\"]), evaluated per feature group at paint time. When present it overrides the constant `color`; a group whose expression doesn't resolve to a color falls back to `color`. Unlocks data-driven stroke colors."
              },
              "dasharray": {
                "description": "On/off lengths in pixels; omit for solid. Each length may be a `$param`.",
                "items": {
                  "oneOf": [
                    {
                      "minimum": 0.0,
                      "type": "number"
                    },
                    {
                      "description": "`$param` reference.",
                      "pattern": "^\\$[A-Za-z_][A-Za-z0-9_-]*$",
                      "type": "string"
                    }
                  ]
                },
                "type": "array"
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "gap-width-expr": {
                "description": "A MapLibre number expression (JSON array) giving the casing gap in pixels, evaluated per feature group at paint time. When present it overrides the constant `gap-width-px`; a group whose expression doesn't resolve to a number falls back to `gap-width-px`."
              },
              "gap-width-px": {
                "default": 0,
                "description": "MapLibre `line-gap-width`: when > 0 the stroke becomes a casing — two parallel bands of `width-px` each, their inner edges this many pixels apart (outer footprint `gap + 2 * width`, centred on the line). `0` strokes the centreline.",
                "type": "number"
              },
              "join": {
                "default": "miter",
                "enum": [
                  "miter",
                  "round",
                  "bevel"
                ],
                "type": "string"
              },
              "op": {
                "const": "stroke",
                "description": "Selects the `stroke` operation."
              },
              "opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "opacity-expr": {
                "description": "A MapLibre number expression (JSON array) giving stroke opacity, evaluated per feature group at paint time. When present it overrides the constant `opacity`; a group whose expression doesn't resolve to a number falls back to `opacity`."
              },
              "width-expr": {
                "description": "A MapLibre number expression (JSON array, e.g. [\"interpolate\", [\"linear\"], [\"zoom\"], 10, 1, 16, 4]) giving stroke width in pixels, evaluated per feature group at paint time. When present it overrides the constant `width-px`; a group whose expression doesn't resolve to a number falls back to `width-px`."
              },
              "width-px": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "features",
              "color"
            ],
            "title": "op: stroke",
            "type": "object"
          },
          {
            "description": "Pick `a` or `b` based on `select` (default `a`). Both inputs accept any port kind. A literal `select` is resolved when the graph is built, and the output mirrors the chosen input's kind — so the two inputs may be different kinds. A `$param` or `@node` `select` is read per render instead, which requires both inputs to be the same kind.",
            "properties": {
              "a": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "b": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "switch",
                "description": "Selects the `switch` operation."
              },
              "select": {
                "default": "a",
                "oneOf": [
                  {
                    "enum": [
                      "a",
                      "b"
                    ],
                    "type": "string"
                  },
                  {
                    "type": "boolean"
                  },
                  {
                    "maximum": 1,
                    "minimum": 0,
                    "type": "integer"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port, read per render. `true` / non-zero picks `b`.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "a",
              "b"
            ],
            "title": "op: switch",
            "type": "object"
          },
          {
            "description": "Text labels (MapLibre `symbol-placement`): `placement: point` (default) labels each feature point, `line` / `line-center` walk each polyline with tangent-rotated glyphs. `font` is an ordered fallback stack of `font` and/or `glyphs` source names; `text` is a literal string or a MapLibre string expression evaluated per feature group. Paint properties have optional `*-expr` siblings; layout knobs are build-time constants in em. Collision is on by default and is deterministic across tiles: candidates come from this tile plus the 8 neighbour tiles (host-bound under `<source>.<layer>@dx,dy`), so borders stay seamless. Set `source`/`layer` (the upstream feature source) to enable neighbour gathering; without them collision is centre-tile-only. `icon-name` / `icon-name-expr` add MapLibre's `icon-image` half: the icon is placed together with the text against the same index, so a symbol shows both or neither unless `icon-optional` / `text-optional` say otherwise. `font` and `text` are required unless the symbol is icon-only.",
            "properties": {
              "allow-overlap": {
                "description": "MapLibre `text-allow-overlap`: place the label even if it collides. It still reserves its box (blocking later labels) unless `ignore-placement`. Default false.",
                "type": "boolean"
              },
              "anchor": {
                "description": "Which part of the label block sits on the point (point placement). Default `center`.",
                "enum": [
                  "center",
                  "left",
                  "right",
                  "top",
                  "bottom",
                  "top-left",
                  "top-right",
                  "bottom-left",
                  "bottom-right"
                ],
                "type": "string"
              },
              "anchor-variants": {
                "description": "MapLibre `text-variable-anchor`: anchors tried in order on collision, the first free one placed (point placement). Overrides `anchor` when set.",
                "items": {
                  "enum": [
                    "center",
                    "left",
                    "right",
                    "top",
                    "bottom",
                    "top-left",
                    "top-right",
                    "bottom-left",
                    "bottom-right"
                  ],
                  "type": "string"
                },
                "type": "array"
              },
              "collide": {
                "description": "Whether to run deterministic label collision. Default true (MapLibre's default). Set false to draw every label (the pre-collision behaviour).",
                "type": "boolean"
              },
              "color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "color-expr": {
                "description": "A MapLibre color expression, evaluated per feature group; overrides the constant `color`."
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "filter-expr": {
                "description": "The upstream `features` filter, reproduced when gathering neighbour candidates so they are filtered identically to this tile's own features. Set it to whatever the `features` node uses."
              },
              "font": {
                "description": "Ordered fallback stack of `font` and/or `glyphs` source names from the document's `sources`. Outline fonts and SDF glyph stacks mix freely; the first entry covering a char shapes it. Also the default/fallback stack for `font-expr` and unlisted `font-stacks` keys.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "font-expr": {
                "description": "A MapLibre expression yielding an array of font names, evaluated per feature group (MapLibre data-driven `text-font`); overrides the constant `font`. Each result is canonicalized (names joined with `,`) and looked up in `font-stacks`; an unlisted stack, a non-array result, or an eval error falls back to `font`. Every stack a host may need must be enumerable in `font-stacks` (wasm hosts pre-bind glyph assets)."
              },
              "font-stacks": {
                "additionalProperties": {
                  "items": {
                    "type": "string"
                  },
                  "type": "array"
                },
                "description": "Named dynamic font stacks: canonical stack key (font names joined with `,`) → ordered `font`/`glyphs` source names. Consulted by `font-expr` and by `format` text sections carrying `text-font`. Declaration order fixes each stack's stable id (used by the layout cache and cross-tile collision).",
                "type": "object"
              },
              "halo-color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "halo-color-expr": {
                "description": "A MapLibre color expression for the halo, evaluated per feature group; overrides the constant `halo-color`."
              },
              "halo-width": {
                "oneOf": [
                  {
                    "description": "Halo radius in px around each glyph. Default 0 (no halo).",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "halo-width-expr": {
                "description": "A MapLibre number expression (px), evaluated per feature group; overrides the constant `halo-width`."
              },
              "icon-allow-overlap": {
                "description": "MapLibre `icon-allow-overlap`. A symbol carries one overlap decision, so with text present this and `allow-overlap` must both be set for the symbol to ignore collision. Default false.",
                "type": "boolean"
              },
              "icon-anchor": {
                "description": "Which part of the icon sits on the point (MapLibre `icon-anchor`). Default `center`.",
                "enum": [
                  "center",
                  "left",
                  "right",
                  "top",
                  "bottom",
                  "top-left",
                  "top-right",
                  "bottom-left",
                  "bottom-right"
                ],
                "type": "string"
              },
              "icon-ignore-placement": {
                "description": "MapLibre `icon-ignore-placement`; with text present it pairs with `ignore-placement` the same way as the overlap flags. Default false.",
                "type": "boolean"
              },
              "icon-name": {
                "description": "Constant MapLibre `icon-image`: the sprite to draw at each point, cropped from `icon-sprite`. Needs `icon-sprite`.",
                "type": "string"
              },
              "icon-name-expr": {
                "description": "A MapLibre expression naming each feature's icon (data-driven `icon-image`), cropped from `icon-sprite`; overrides `icon-name`. A group resolving to an empty/unknown name draws no icon and places as a text-only symbol."
              },
              "icon-offset": {
                "description": "MapLibre `icon-offset`: shift [x, y] in px, itself scaled by `icon-size`. Default [0, 0].",
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "icon-opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "icon-opacity-expr": {
                "description": "A MapLibre number expression giving the icon's opacity; overrides `icon-opacity`."
              },
              "icon-optional": {
                "description": "MapLibre `icon-optional`: let the text place when the icon's box is blocked. Default false — icon and text place as one unit.",
                "type": "boolean"
              },
              "icon-padding-expr": {
                "description": "A MapLibre number expression (px), evaluated per feature group; overrides `icon-padding-px`."
              },
              "icon-padding-px": {
                "description": "MapLibre `icon-padding`: collision-box inflation around the icon, in px. Default 2.",
                "minimum": 0.0,
                "type": "number"
              },
              "icon-rotate-deg": {
                "description": "MapLibre `icon-rotate`: rotation in degrees clockwise about the icon centre. Default 0.",
                "type": "number"
              },
              "icon-rotate-deg-expr": {
                "description": "A MapLibre number expression (degrees clockwise); overrides `icon-rotate-deg`."
              },
              "icon-size": {
                "description": "MapLibre `icon-size`: multiplier on the sprite's authored display size (atlas pixels over its pixel ratio). Default 1.",
                "minimum": 0.0,
                "type": "number"
              },
              "icon-size-expr": {
                "description": "A MapLibre number expression, evaluated per feature group; overrides `icon-size`."
              },
              "icon-sprite": {
                "description": "Asset reference (`@name`) or literal path.",
                "type": "string"
              },
              "icon-text-fit": {
                "description": "MapLibre `icon-text-fit`: stretch the icon to the box its label occupies, on the named axes. The sprite's `stretchX`/`stretchY`/`content` metadata decides which bands absorb the growth, so a shield keeps its corners; a sprite without it scales whole. The fitted box is also the icon's collision box. Default `none`.",
                "enum": [
                  "none",
                  "width",
                  "height",
                  "both"
                ]
              },
              "icon-text-fit-padding": {
                "description": "MapLibre `icon-text-fit-padding`: `[top, right, bottom, left]` px added around the label's box before the icon is fitted to it. Default [0, 0, 0, 0].",
                "items": {
                  "type": "number"
                },
                "maxItems": 4,
                "minItems": 4,
                "type": "array"
              },
              "ignore-placement": {
                "description": "MapLibre `text-ignore-placement`: don't let this label block later ones (skip inserting its collision box). Default false.",
                "type": "boolean"
              },
              "justify": {
                "description": "Line alignment within the wrapped block. Default `auto` (follows the anchor's horizontal side).",
                "enum": [
                  "auto",
                  "left",
                  "center",
                  "right"
                ],
                "type": "string"
              },
              "keep-upright": {
                "description": "Flip a right-to-left label so it reads upright (MapLibre `text-keep-upright`). Line placement only. Default true.",
                "type": "boolean"
              },
              "layer": {
                "description": "Upstream feature layer name (with `source`). Used only to name neighbour bindings for cross-tile collision.",
                "type": "string"
              },
              "letter-spacing-em": {
                "description": "Extra advance per glyph in em. Default 0.",
                "type": "number"
              },
              "line-height": {
                "description": "Baseline-to-baseline distance in em. Default 1.2.",
                "type": "number"
              },
              "max-angle-deg": {
                "description": "Max cumulative bend in degrees the line may turn over a label-length window before the anchor is rejected (MapLibre `text-max-angle`). Line placement only. Default 45.",
                "minimum": 0.0,
                "type": "number"
              },
              "max-extent-px": {
                "description": "Canvas pad this node requests; labels whose bbox half-extent exceeds it are culled with a warning. Default 128.",
                "minimum": 0.0,
                "type": "number"
              },
              "max-width-em": {
                "description": "Wrap target width in em. Default 10; 0 disables wrapping.",
                "minimum": 0.0,
                "type": "number"
              },
              "max-zoom": {
                "description": "Style layer zoom gate: draw nothing above this zoom (mirrors the `features` node).",
                "maximum": 24,
                "minimum": 0,
                "type": "integer"
              },
              "min-zoom": {
                "description": "Style layer zoom gate: draw nothing below this zoom (mirrors the `features` node).",
                "maximum": 24,
                "minimum": 0,
                "type": "integer"
              },
              "min-zoom-field": {
                "description": "Per-feature `min_zoom` property name, reproduced for neighbour candidate filtering (mirrors the `features` node).",
                "type": "string"
              },
              "offset-em": {
                "description": "Block shift [x, y] in em, applied after anchoring. Default [0, 0].",
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "op": {
                "const": "text",
                "description": "Selects the `text` operation."
              },
              "opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "opacity-expr": {
                "description": "A MapLibre number expression giving opacity, evaluated per feature group; multiplies both fill and halo alpha. Overrides the constant `opacity`."
              },
              "outline-sdf": {
                "description": "Render outline-font glyphs through the SDF field-sampling path (maplibre-gl-js style, which draws every glyph from an SDF) instead of a per-glyph vector fill / stroke. The halo then comes from a distance threshold rather than a stroke, matching MapLibre's halo shape and cost. SDF glyph (`glyphs`) stacks are unaffected. Default true.",
                "type": "boolean"
              },
              "padding-expr": {
                "description": "A MapLibre number expression (MapLibre `text-padding`), evaluated per feature group; overrides the constant `padding-px` for that group's collision boxes."
              },
              "padding-px": {
                "description": "Collision-box inflation in px on every side. Default 2.",
                "minimum": 0.0,
                "type": "number"
              },
              "placement": {
                "description": "MapLibre `symbol-placement`. `point` (default) labels each feature point. `line` repeats labels along each polyline every `spacing-px`; `line-center` places one at each line's arc-length midpoint. Line placement ignores wrapping (`max-width-em`) and lays out a single line along the path.",
                "enum": [
                  "point",
                  "line",
                  "line-center"
                ],
                "type": "string"
              },
              "radial-offset": {
                "description": "MapLibre `text-radial-offset`: em distance each `anchor-variants` entry pushes the block away from the point in its direction. Default 0.",
                "type": "number"
              },
              "size": {
                "oneOf": [
                  {
                    "description": "Font size in px. Default 16.",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "size-expr": {
                "description": "A MapLibre number expression (px), evaluated per feature group; overrides the constant `size`."
              },
              "sort-key-expr": {
                "description": "A MapLibre number expression (MapLibre `symbol-sort-key`), evaluated per feature group; lower values place first under collision. Absent = 0."
              },
              "source": {
                "description": "Upstream feature source name (matches the `features` node). Used only to name the neighbour tile bindings for cross-tile collision. Omit to collide within this tile only.",
                "type": "string"
              },
              "spacing-px": {
                "description": "Gap in px between successive line anchors (MapLibre `symbol-spacing`). Line placement only. Default 250.",
                "minimum": 1.0,
                "type": "number"
              },
              "text": {
                "description": "The label: a literal string, or a MapLibre string expression (evaluated per feature group; empty/failed → the group draws nothing)."
              },
              "text-optional": {
                "description": "MapLibre `text-optional`: let the icon place when the text's box is blocked. Default false.",
                "type": "boolean"
              },
              "transform": {
                "description": "Case transform applied before shaping. Default `none`.",
                "enum": [
                  "none",
                  "uppercase",
                  "lowercase"
                ],
                "type": "string"
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: text",
            "type": "object"
          },
          {
            "description": "Draw one label layer's placed labels: `labels` is the layer's `text-labels` node, `placement` the shared `label-placement` node that decided it (wire the same `text-labels` node into both). Styling and layout live on `text-labels`; this node only rasterizes the winners.",
            "properties": {
              "labels": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "max-extent-px": {
                "description": "Canvas pad (px) this layer's labels need to cross a tile border un-clipped. Mirrors the `text-labels` field of the same name.",
                "minimum": 0,
                "type": "number"
              },
              "op": {
                "const": "text-draw",
                "description": "Selects the `text-draw` operation."
              },
              "placement": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              }
            },
            "required": [
              "op",
              "labels",
              "placement"
            ],
            "title": "op: text-draw",
            "type": "object"
          },
          {
            "description": "Text labels (MapLibre `symbol-placement`): `placement: point` (default) labels each feature point, `line` / `line-center` walk each polyline with tangent-rotated glyphs. `font` is an ordered fallback stack of `font` and/or `glyphs` source names; `text` is a literal string or a MapLibre string expression evaluated per feature group. Paint properties have optional `*-expr` siblings; layout knobs are build-time constants in em. Collision is on by default and is deterministic across tiles: candidates come from this tile plus the 8 neighbour tiles (host-bound under `<source>.<layer>@dx,dy`), so borders stay seamless. Set `source`/`layer` (the upstream feature source) to enable neighbour gathering; without them collision is centre-tile-only. `icon-name` / `icon-name-expr` add MapLibre's `icon-image` half: the icon is placed together with the text against the same index, so a symbol shows both or neither unless `icon-optional` / `text-optional` say otherwise. `font` and `text` are required unless the symbol is icon-only. Emits placement candidates instead of pixels: wire this node into a `label-placement` node and draw the result with `text-draw`.",
            "properties": {
              "allow-overlap": {
                "description": "MapLibre `text-allow-overlap`: place the label even if it collides. It still reserves its box (blocking later labels) unless `ignore-placement`. Default false.",
                "type": "boolean"
              },
              "anchor": {
                "description": "Which part of the label block sits on the point (point placement). Default `center`.",
                "enum": [
                  "center",
                  "left",
                  "right",
                  "top",
                  "bottom",
                  "top-left",
                  "top-right",
                  "bottom-left",
                  "bottom-right"
                ],
                "type": "string"
              },
              "anchor-variants": {
                "description": "MapLibre `text-variable-anchor`: anchors tried in order on collision, the first free one placed (point placement). Overrides `anchor` when set.",
                "items": {
                  "enum": [
                    "center",
                    "left",
                    "right",
                    "top",
                    "bottom",
                    "top-left",
                    "top-right",
                    "bottom-left",
                    "bottom-right"
                  ],
                  "type": "string"
                },
                "type": "array"
              },
              "collide": {
                "description": "Whether to run deterministic label collision. Default true (MapLibre's default). Set false to draw every label (the pre-collision behaviour).",
                "type": "boolean"
              },
              "color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "color-expr": {
                "description": "A MapLibre color expression, evaluated per feature group; overrides the constant `color`."
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "filter-expr": {
                "description": "The upstream `features` filter, reproduced when gathering neighbour candidates so they are filtered identically to this tile's own features. Set it to whatever the `features` node uses."
              },
              "font": {
                "description": "Ordered fallback stack of `font` and/or `glyphs` source names from the document's `sources`. Outline fonts and SDF glyph stacks mix freely; the first entry covering a char shapes it. Also the default/fallback stack for `font-expr` and unlisted `font-stacks` keys.",
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "font-expr": {
                "description": "A MapLibre expression yielding an array of font names, evaluated per feature group (MapLibre data-driven `text-font`); overrides the constant `font`. Each result is canonicalized (names joined with `,`) and looked up in `font-stacks`; an unlisted stack, a non-array result, or an eval error falls back to `font`. Every stack a host may need must be enumerable in `font-stacks` (wasm hosts pre-bind glyph assets)."
              },
              "font-stacks": {
                "additionalProperties": {
                  "items": {
                    "type": "string"
                  },
                  "type": "array"
                },
                "description": "Named dynamic font stacks: canonical stack key (font names joined with `,`) → ordered `font`/`glyphs` source names. Consulted by `font-expr` and by `format` text sections carrying `text-font`. Declaration order fixes each stack's stable id (used by the layout cache and cross-tile collision).",
                "type": "object"
              },
              "halo-color": {
                "description": "sRGB hex color, `$param` reference, or `@node` scalar port.",
                "pattern": "^(#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?|[$@][A-Za-z_][A-Za-z0-9_-]*)$",
                "type": "string"
              },
              "halo-color-expr": {
                "description": "A MapLibre color expression for the halo, evaluated per feature group; overrides the constant `halo-color`."
              },
              "halo-width": {
                "oneOf": [
                  {
                    "description": "Halo radius in px around each glyph. Default 0 (no halo).",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "halo-width-expr": {
                "description": "A MapLibre number expression (px), evaluated per feature group; overrides the constant `halo-width`."
              },
              "icon-allow-overlap": {
                "description": "MapLibre `icon-allow-overlap`. A symbol carries one overlap decision, so with text present this and `allow-overlap` must both be set for the symbol to ignore collision. Default false.",
                "type": "boolean"
              },
              "icon-anchor": {
                "description": "Which part of the icon sits on the point (MapLibre `icon-anchor`). Default `center`.",
                "enum": [
                  "center",
                  "left",
                  "right",
                  "top",
                  "bottom",
                  "top-left",
                  "top-right",
                  "bottom-left",
                  "bottom-right"
                ],
                "type": "string"
              },
              "icon-ignore-placement": {
                "description": "MapLibre `icon-ignore-placement`; with text present it pairs with `ignore-placement` the same way as the overlap flags. Default false.",
                "type": "boolean"
              },
              "icon-name": {
                "description": "Constant MapLibre `icon-image`: the sprite to draw at each point, cropped from `icon-sprite`. Needs `icon-sprite`.",
                "type": "string"
              },
              "icon-name-expr": {
                "description": "A MapLibre expression naming each feature's icon (data-driven `icon-image`), cropped from `icon-sprite`; overrides `icon-name`. A group resolving to an empty/unknown name draws no icon and places as a text-only symbol."
              },
              "icon-offset": {
                "description": "MapLibre `icon-offset`: shift [x, y] in px, itself scaled by `icon-size`. Default [0, 0].",
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "icon-opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "icon-opacity-expr": {
                "description": "A MapLibre number expression giving the icon's opacity; overrides `icon-opacity`."
              },
              "icon-optional": {
                "description": "MapLibre `icon-optional`: let the text place when the icon's box is blocked. Default false — icon and text place as one unit.",
                "type": "boolean"
              },
              "icon-padding-expr": {
                "description": "A MapLibre number expression (px), evaluated per feature group; overrides `icon-padding-px`."
              },
              "icon-padding-px": {
                "description": "MapLibre `icon-padding`: collision-box inflation around the icon, in px. Default 2.",
                "minimum": 0.0,
                "type": "number"
              },
              "icon-rotate-deg": {
                "description": "MapLibre `icon-rotate`: rotation in degrees clockwise about the icon centre. Default 0.",
                "type": "number"
              },
              "icon-rotate-deg-expr": {
                "description": "A MapLibre number expression (degrees clockwise); overrides `icon-rotate-deg`."
              },
              "icon-size": {
                "description": "MapLibre `icon-size`: multiplier on the sprite's authored display size (atlas pixels over its pixel ratio). Default 1.",
                "minimum": 0.0,
                "type": "number"
              },
              "icon-size-expr": {
                "description": "A MapLibre number expression, evaluated per feature group; overrides `icon-size`."
              },
              "icon-sprite": {
                "description": "Asset reference (`@name`) or literal path.",
                "type": "string"
              },
              "icon-text-fit": {
                "description": "MapLibre `icon-text-fit`: stretch the icon to the box its label occupies, on the named axes. The sprite's `stretchX`/`stretchY`/`content` metadata decides which bands absorb the growth, so a shield keeps its corners; a sprite without it scales whole. The fitted box is also the icon's collision box. Default `none`.",
                "enum": [
                  "none",
                  "width",
                  "height",
                  "both"
                ]
              },
              "icon-text-fit-padding": {
                "description": "MapLibre `icon-text-fit-padding`: `[top, right, bottom, left]` px added around the label's box before the icon is fitted to it. Default [0, 0, 0, 0].",
                "items": {
                  "type": "number"
                },
                "maxItems": 4,
                "minItems": 4,
                "type": "array"
              },
              "ignore-placement": {
                "description": "MapLibre `text-ignore-placement`: don't let this label block later ones (skip inserting its collision box). Default false.",
                "type": "boolean"
              },
              "justify": {
                "description": "Line alignment within the wrapped block. Default `auto` (follows the anchor's horizontal side).",
                "enum": [
                  "auto",
                  "left",
                  "center",
                  "right"
                ],
                "type": "string"
              },
              "keep-upright": {
                "description": "Flip a right-to-left label so it reads upright (MapLibre `text-keep-upright`). Line placement only. Default true.",
                "type": "boolean"
              },
              "layer": {
                "description": "Upstream feature layer name (with `source`). Used only to name neighbour bindings for cross-tile collision.",
                "type": "string"
              },
              "letter-spacing-em": {
                "description": "Extra advance per glyph in em. Default 0.",
                "type": "number"
              },
              "line-height": {
                "description": "Baseline-to-baseline distance in em. Default 1.2.",
                "type": "number"
              },
              "max-angle-deg": {
                "description": "Max cumulative bend in degrees the line may turn over a label-length window before the anchor is rejected (MapLibre `text-max-angle`). Line placement only. Default 45.",
                "minimum": 0.0,
                "type": "number"
              },
              "max-extent-px": {
                "description": "Canvas pad this node requests; labels whose bbox half-extent exceeds it are culled with a warning. Default 128.",
                "minimum": 0.0,
                "type": "number"
              },
              "max-width-em": {
                "description": "Wrap target width in em. Default 10; 0 disables wrapping.",
                "minimum": 0.0,
                "type": "number"
              },
              "max-zoom": {
                "description": "Style layer zoom gate: draw nothing above this zoom (mirrors the `features` node).",
                "maximum": 24,
                "minimum": 0,
                "type": "integer"
              },
              "min-zoom": {
                "description": "Style layer zoom gate: draw nothing below this zoom (mirrors the `features` node).",
                "maximum": 24,
                "minimum": 0,
                "type": "integer"
              },
              "min-zoom-field": {
                "description": "Per-feature `min_zoom` property name, reproduced for neighbour candidate filtering (mirrors the `features` node).",
                "type": "string"
              },
              "offset-em": {
                "description": "Block shift [x, y] in em, applied after anchoring. Default [0, 0].",
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "op": {
                "const": "text-labels",
                "description": "Selects the `text-labels` operation."
              },
              "opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "opacity-expr": {
                "description": "A MapLibre number expression giving opacity, evaluated per feature group; multiplies both fill and halo alpha. Overrides the constant `opacity`."
              },
              "outline-sdf": {
                "description": "Render outline-font glyphs through the SDF field-sampling path (maplibre-gl-js style, which draws every glyph from an SDF) instead of a per-glyph vector fill / stroke. The halo then comes from a distance threshold rather than a stroke, matching MapLibre's halo shape and cost. SDF glyph (`glyphs`) stacks are unaffected. Default true.",
                "type": "boolean"
              },
              "padding-expr": {
                "description": "A MapLibre number expression (MapLibre `text-padding`), evaluated per feature group; overrides the constant `padding-px` for that group's collision boxes."
              },
              "padding-px": {
                "description": "Collision-box inflation in px on every side. Default 2.",
                "minimum": 0.0,
                "type": "number"
              },
              "placement": {
                "description": "MapLibre `symbol-placement`. `point` (default) labels each feature point. `line` repeats labels along each polyline every `spacing-px`; `line-center` places one at each line's arc-length midpoint. Line placement ignores wrapping (`max-width-em`) and lays out a single line along the path.",
                "enum": [
                  "point",
                  "line",
                  "line-center"
                ],
                "type": "string"
              },
              "radial-offset": {
                "description": "MapLibre `text-radial-offset`: em distance each `anchor-variants` entry pushes the block away from the point in its direction. Default 0.",
                "type": "number"
              },
              "size": {
                "oneOf": [
                  {
                    "description": "Font size in px. Default 16.",
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "size-expr": {
                "description": "A MapLibre number expression (px), evaluated per feature group; overrides the constant `size`."
              },
              "sort-key-expr": {
                "description": "A MapLibre number expression (MapLibre `symbol-sort-key`), evaluated per feature group; lower values place first under collision. Absent = 0."
              },
              "source": {
                "description": "Upstream feature source name (matches the `features` node). Used only to name the neighbour tile bindings for cross-tile collision. Omit to collide within this tile only.",
                "type": "string"
              },
              "spacing-px": {
                "description": "Gap in px between successive line anchors (MapLibre `symbol-spacing`). Line placement only. Default 250.",
                "minimum": 1.0,
                "type": "number"
              },
              "text": {
                "description": "The label: a literal string, or a MapLibre string expression (evaluated per feature group; empty/failed → the group draws nothing)."
              },
              "text-optional": {
                "description": "MapLibre `text-optional`: let the icon place when the text's box is blocked. Default false.",
                "type": "boolean"
              },
              "transform": {
                "description": "Case transform applied before shaping. Default `none`.",
                "enum": [
                  "none",
                  "uppercase",
                  "lowercase"
                ],
                "type": "string"
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: text-labels",
            "type": "object"
          },
          {
            "description": "Binarise a scalar field: emit `low` for samples ≤ `value`, `high` otherwise. With `softness > 0` the transition is a linear ramp of width `softness` centred on `value`.",
            "properties": {
              "field": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "high": {
                "oneOf": [
                  {
                    "default": 1.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "low": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "op": {
                "const": "threshold",
                "description": "Selects the `threshold` operation."
              },
              "softness": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "value": {
                "oneOf": [
                  {
                    "default": 0.5,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "field"
            ],
            "title": "op: threshold",
            "type": "object"
          },
          {
            "description": "Tile-filling rectangle polygon source.",
            "properties": {
              "extent": {
                "default": 4096,
                "minimum": 1,
                "type": "integer"
              },
              "op": {
                "const": "tile-bounds",
                "description": "Selects the `tile-bounds` operation."
              }
            },
            "required": [
              "op"
            ],
            "title": "op: tile-bounds",
            "type": "object"
          },
          {
            "description": "Repeat a raster across the canvas with bilinear wrap-around sampling. Primary use case is paper / canvas texture composition. Does not stretch-to-fit — use a dedicated `cover`/`contain` node if that's what you want.",
            "properties": {
              "anchor": {
                "default": "world",
                "description": "`world` (default) makes the pattern seamless across map tiles; `tile` restarts the pattern at every map tile's top-left.",
                "enum": [
                  "world",
                  "tile"
                ],
                "type": "string"
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "offset-px": {
                "description": "Pattern offset in canvas pixels [x, y].",
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "op": {
                "const": "tiling",
                "description": "Selects the `tiling` operation."
              },
              "opacity": {
                "oneOf": [
                  {
                    "maximum": 1.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "rotation-deg": {
                "oneOf": [
                  {
                    "description": "Rotation of the pattern in degrees (clockwise).",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "scale-px": {
                "description": "Canvas pixels per one source repetition. Defaults to the source image's native width.",
                "minimum": 0.5,
                "type": "number"
              }
            },
            "required": [
              "op",
              "input"
            ],
            "title": "op: tiling",
            "type": "object"
          },
          {
            "description": "Translate / rotate / scale every input vertex. Rotation happens around `pivot` (defaults to the origin) before the final translation. The scalar fields take a `$param` or an `@node` port and follow it per render; the `[x, y]` arrays are literal, and seed the matching scalars. Uniform `scale` multiplies the per-axis factors.",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "transform",
                "description": "Selects the `transform` operation."
              },
              "pivot": {
                "description": "Literal [x, y] rotation centre in feature space.",
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "rotation-deg": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "scale": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "scale-x": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "scale-xy": {
                "description": "Literal per-axis scale. `scale-x` / `scale-y` override it; uniform `scale` multiplies it.",
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "scale-y": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "translate": {
                "description": "Literal [x, y] shift. `translate-x` / `translate-y` override it.",
                "items": {
                  "type": "number"
                },
                "maxItems": 2,
                "minItems": 2,
                "type": "array"
              },
              "translate-x": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "translate-y": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: transform",
            "type": "object"
          },
          {
            "description": "Delaunay triangulation of the input's point set. Each triangle becomes a closed polygon. Polygons and lines on input are ignored.",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "triangulate",
                "description": "Selects the `triangulate` operation."
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: triangulate",
            "type": "object"
          },
          {
            "description": "Adaptively boost CIELAB chroma — low-chroma pixels rise more than already-vivid ones (`amount` = strength, 0 = identity). Hue and lightness preserved.",
            "properties": {
              "amount": {
                "oneOf": [
                  {
                    "default": 0.0,
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "vibrance",
                "description": "Selects the `vibrance` operation."
              }
            },
            "required": [
              "op",
              "input"
            ],
            "title": "op: vibrance",
            "type": "object"
          },
          {
            "description": "Voronoi diagram of the input's point set. Output is a Features value carrying the Voronoi edges as polylines (each polyline is a single 2-point edge). Polygons and lines in the input are ignored — use `centroid` to derive points from them.",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "voronoi",
                "description": "Selects the `voronoi` operation."
              }
            },
            "required": [
              "op",
              "features"
            ],
            "title": "op: voronoi",
            "type": "object"
          },
          {
            "description": "Fracture each polygon in `features` into Voronoi sub-cells using the points in `seeds` as Voronoi sites. Cells are clipped against the source polygon. Lines/points on `features` and lines/polygons on `seeds` are ignored.",
            "properties": {
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "op": {
                "const": "voronoi-fracture",
                "description": "Selects the `voronoi-fracture` operation."
              },
              "seeds": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              }
            },
            "required": [
              "op",
              "features",
              "seeds"
            ],
            "title": "op: voronoi-fracture",
            "type": "object"
          },
          {
            "description": "Domain warp via an internal noise field. With `anchor: world` (default) the warp is seamless across tile borders; the upstream pad grows by `amp-px` to keep samples inside the available raster.",
            "properties": {
              "amp-px": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "amp-x-px": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "amp-x-px-max": {
                "description": "Upper bound on `amp-x-px` for padding, required when `amp-x-px` is an `@node` port. Values above it are clamped.",
                "minimum": 0.0,
                "type": "number"
              },
              "amp-y-px": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "amp-y-px-max": {
                "description": "Upper bound on `amp-y-px` for padding, required when `amp-y-px` is an `@node` port. Values above it are clamped.",
                "minimum": 0.0,
                "type": "number"
              },
              "anchor": {
                "default": "world",
                "enum": [
                  "tile",
                  "world"
                ],
                "type": "string"
              },
              "boundary": {
                "default": "clamp",
                "enum": [
                  "clamp",
                  "transparent",
                  "mirror"
                ],
                "type": "string"
              },
              "gain": {
                "oneOf": [
                  {
                    "default": 0.5,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "input": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "lacunarity": {
                "oneOf": [
                  {
                    "default": 2.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "octaves": {
                "default": 1,
                "maximum": 12,
                "minimum": 1,
                "type": "integer"
              },
              "op": {
                "const": "warp",
                "description": "Selects the `warp` operation."
              },
              "scale-px": {
                "oneOf": [
                  {
                    "minimum": 0.0,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "seed": {
                "description": "Field seed. Omitted, `anchor: world` uses a fixed seed so every tile samples the same field, and `anchor: tile` uses the host's per-tile seed so each tile gets its own. Set it to pin either.",
                "minimum": 0,
                "type": "integer"
              },
              "type": {
                "default": "perlin",
                "enum": [
                  "white",
                  "value",
                  "perlin",
                  "simplex",
                  "worley"
                ],
                "type": "string"
              }
            },
            "required": [
              "op",
              "input",
              "scale-px",
              "amp-px"
            ],
            "title": "op: warp",
            "type": "object"
          },
          {
            "description": "Displace polylines laterally with a sine wave. Lengths in canvas pixels. Sharp corners may kink (no tangent smoothing). Polygons/points pass through.",
            "properties": {
              "amplitude-px": {
                "oneOf": [
                  {
                    "description": "Peak lateral deviation in pixels. May be negative to flip phase.",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "features": {
                "description": "Reference to another node (`@name`).",
                "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*$",
                "type": "string"
              },
              "noise-amp-px": {
                "oneOf": [
                  {
                    "description": "Peak 1D-value-noise jitter added on top of the sine, in pixels. 0 = pure sine (default).",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "noise-scale-px": {
                "oneOf": [
                  {
                    "description": "Cell length of the noise jitter along arc length, in pixels. Defaults to `wavelength-px`.",
                    "minimum": 0.5,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "op": {
                "const": "wave",
                "description": "Selects the `wave` operation."
              },
              "phase-px": {
                "oneOf": [
                  {
                    "description": "Offset into the wave at the polyline start, in pixels.",
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              },
              "samples-per-wavelength": {
                "description": "Subdivisions per wavelength. Higher = smoother but more vertices. Default 16.",
                "maximum": 256,
                "minimum": 2,
                "type": "number"
              },
              "seed": {
                "description": "Optional explicit u32 seed for the world-anchored 2D value noise. Default: a fixed constant so adjacent tiles agree across the seam.",
                "minimum": 0,
                "type": "integer"
              },
              "wavelength-px": {
                "oneOf": [
                  {
                    "minimum": 0.5,
                    "type": "number"
                  },
                  {
                    "description": "`$param` reference or `@node` scalar port.",
                    "pattern": "^[$@][A-Za-z_][A-Za-z0-9_-]*$",
                    "type": "string"
                  }
                ]
              }
            },
            "required": [
              "op",
              "features",
              "amplitude-px",
              "wavelength-px"
            ],
            "title": "op: wave",
            "type": "object"
          },
          {
            "description": "The current tile's zoom level as a scalar number. Combine with `math` for zoom-dependent styling.",
            "properties": {
              "op": {
                "const": "zoom",
                "description": "Selects the `zoom` operation."
              }
            },
            "required": [
              "op"
            ],
            "title": "op: zoom",
            "type": "object"
          },
          {
            "additionalProperties": true,
            "description": "Call a user-defined function declared in the document's `functions` block.",
            "properties": {
              "fn": {
                "description": "Name of the function to call.",
                "type": "string"
              },
              "op": {
                "const": "func"
              }
            },
            "required": [
              "op",
              "fn"
            ],
            "title": "op: func",
            "type": "object"
          }
        ]
      },
      "type": "object"
    },
    "output": {
      "description": "Node id of the final raster (with or without `@`).",
      "type": "string"
    },
    "pad": {
      "minimum": 0,
      "type": "integer"
    },
    "params": {
      "additionalProperties": {
        "properties": {
          "default": {},
          "description": {
            "type": "string"
          },
          "max": {
            "type": "number"
          },
          "min": {
            "type": "number"
          },
          "type": {
            "enum": [
              "color",
              "number",
              "bool"
            ]
          }
        },
        "required": [
          "type",
          "default"
        ],
        "type": "object"
      },
      "type": "object"
    },
    "sources": {
      "additionalProperties": {
        "oneOf": [
          {
            "properties": {
              "attribution": {
                "type": "string"
              },
              "src": {
                "type": "string"
              },
              "type": {
                "enum": [
                  "brush",
                  "image"
                ]
              }
            },
            "required": [
              "type",
              "src"
            ],
            "type": "object"
          },
          {
            "properties": {
              "attribution": {
                "description": "Explicit attribution; inherits upstream TileJSON / PMTiles metadata when absent.",
                "type": "string"
              },
              "type": {
                "enum": [
                  "mvt",
                  "pmtiles"
                ]
              },
              "url": {
                "type": "string"
              }
            },
            "required": [
              "type",
              "url"
            ],
            "type": "object"
          },
          {
            "properties": {
              "attribution": {
                "type": "string"
              },
              "elevation-offset": {
                "type": "number"
              },
              "encoding": {
                "enum": [
                  "terrarium",
                  "mapbox-rgb"
                ]
              },
              "max-zoom": {
                "description": "Deepest zoom the source serves. Past it, tiles are overzoomed from the covering ancestor.",
                "minimum": 0,
                "type": "integer"
              },
              "neighbor-fetch": {
                "default": true,
                "type": "boolean"
              },
              "on-missing": {
                "default": "empty",
                "description": "404 within zoom range: zero elevation, upsample a parent, or fail the tile.",
                "enum": [
                  "empty",
                  "upsample",
                  "error"
                ]
              },
              "tile-size": {
                "default": 256,
                "description": "Edge of the source's own tiles in px — not the document's canvas, and defaulting to 256 rather than 512. Terrarium pyramids are usually 256, Mapbox-RGB ones often 512.",
                "minimum": 1,
                "type": "integer"
              },
              "type": {
                "const": "dem"
              },
              "url": {
                "description": "XYZ template or TileJSON URL.",
                "type": "string"
              }
            },
            "required": [
              "type",
              "url",
              "encoding"
            ],
            "type": "object"
          },
          {
            "properties": {
              "attribution": {
                "type": "string"
              },
              "max-zoom": {
                "minimum": 0,
                "type": "integer"
              },
              "neighbor-fetch": {
                "type": "boolean"
              },
              "on-missing": {
                "default": "empty",
                "description": "404 within zoom range: transparent pixels, upsample a parent, or fail the tile.",
                "enum": [
                  "empty",
                  "upsample",
                  "error"
                ]
              },
              "type": {
                "const": "raster"
              },
              "url": {
                "description": "XYZ template, TileJSON URL, or PMTiles archive (`.pmtiles`). PNG/WebP/JPEG tiles.",
                "type": "string"
              }
            },
            "required": [
              "type",
              "url"
            ],
            "type": "object"
          }
        ]
      },
      "type": "object"
    },
    "tile-size": {
      "default": 512,
      "description": "Rendered tile edge in px. Every `*-px` field is measured against it, so it decides what a pixel is worth in ground units. Defaults to 512, MapLibre's vector-tile convention.",
      "minimum": 1,
      "type": "integer"
    },
    "version": {
      "type": "string"
    }
  },
  "required": [
    "name",
    "nodes",
    "output"
  ],
  "title": "Ezu Style Spec",
  "type": "object"
}
