{
  "schema_version": 3,
  "modules": [
    {
      "name": "Scene",
      "group": "engine",
      "category": "Scene & rendering",
      "docs": "Declarative 3D scene nodes, materials, models, animation, and transforms.\n\nConstructors produce immutable scene values. Most modifiers take the scene\nlast so a node can be built as a readable pipeline.\n\nCoordinates are Y-up and right-handed: +Y is up, +X is right, and the\nground is the XZ plane.\n\nTransforms wrap outward, so the OUTER call applies last in world space —\n`s |> Scene.rotateY(r) |> Scene.translate(v)` rotates in place and then\nmoves, which is the order the source reads. The primitives take no size\narguments, so a box is `Scene.cube() |> Scene.scaleXYZ(w, h, d)`.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Scene.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque scene node."
        },
        {
          "name": "cube",
          "qualified_name": "Scene.cube",
          "kind": "value",
          "declaration": "let cube : () => t",
          "docs": "Create a unit cube centered at the origin."
        },
        {
          "name": "sphere",
          "qualified_name": "Scene.sphere",
          "kind": "value",
          "declaration": "let sphere : () => t",
          "docs": "Create a unit sphere centered at the origin."
        },
        {
          "name": "cylinder",
          "qualified_name": "Scene.cylinder",
          "kind": "value",
          "declaration": "let cylinder : () => t",
          "docs": "Create a unit cylinder aligned to the Y axis."
        },
        {
          "name": "quad",
          "qualified_name": "Scene.quad",
          "kind": "value",
          "declaration": "let quad : () => t",
          "docs": "Create a unit quad in the XY plane, facing `+Z`."
        },
        {
          "name": "plane",
          "qualified_name": "Scene.plane",
          "kind": "value",
          "declaration": "let plane : () => t",
          "docs": "Create a unit plane in the XZ ground plane."
        },
        {
          "name": "model",
          "qualified_name": "Scene.model",
          "kind": "value",
          "declaration": "let model : (Asset.Model) => t",
          "docs": "Create a scene node from a model asset.\n\nA locator whose file is missing logs an error and renders the empty\nfallback asset, so a mistyped or unfetched model shows as nothing rather\nthan failing the frame."
        },
        {
          "name": "heightmap",
          "qualified_name": "Scene.heightmap",
          "kind": "value",
          "declaration": "let heightmap : (List<List<float>>) => t",
          "docs": "Create terrain from a rectangular grid of height values."
        },
        {
          "name": "terrain",
          "qualified_name": "Scene.terrain",
          "kind": "value",
          "declaration": "let terrain : (Terrain.t) => t",
          "docs": "Create a scene node from an asset-backed terrain descriptor."
        },
        {
          "name": "group",
          "qualified_name": "Scene.group",
          "kind": "value",
          "declaration": "let group : (List<t>) => t",
          "docs": "Group scene nodes under one transform."
        },
        {
          "name": "color",
          "qualified_name": "Scene.color",
          "kind": "value",
          "declaration": "let color : (Color.t, t) => t",
          "docs": "Apply an unlit solid color; the scene is last for piping."
        },
        {
          "name": "lit",
          "qualified_name": "Scene.lit",
          "kind": "value",
          "declaration": "let lit : (Color.t, t) => t",
          "docs": "Apply a lit solid-color material; the scene is last for piping. It needs\nlights to be visible — under a plain `Frame.create` it renders black."
        },
        {
          "name": "emissive",
          "qualified_name": "Scene.emissive",
          "kind": "value",
          "declaration": "let emissive : (Color.t, t) => t",
          "docs": "Apply an emissive solid-color material; the scene is last for piping."
        },
        {
          "name": "litTexture",
          "qualified_name": "Scene.litTexture",
          "kind": "value",
          "declaration": "let litTexture : ('texture, t) => t",
          "docs": "Apply a lit texture from `Texture.t` or `Asset.Texture`."
        },
        {
          "name": "emissiveTexture",
          "qualified_name": "Scene.emissiveTexture",
          "kind": "value",
          "declaration": "let emissiveTexture : ('texture, t) => t",
          "docs": "Apply an emissive texture from `Texture.t` or `Asset.Texture`."
        },
        {
          "name": "litNormalMapped",
          "qualified_name": "Scene.litNormalMapped",
          "kind": "value",
          "declaration": "let litNormalMapped : (Color.t, 'texture, t) => t",
          "docs": "Apply a lit color and a TANGENT-SPACE normal-map texture.\n\nThe map perturbs the surface normal used for lighting, so its bumps catch\nthe scene's diffuse and specular response without changing the geometry."
        },
        {
          "name": "screen",
          "qualified_name": "Scene.screen",
          "kind": "value",
          "declaration": "let screen : (RenderTarget.t, t) => t",
          "docs": "Display a render target on this surface.\n\nThe surface is emissive, so the feed is shown unlit. A target no frame\nwrites renders magenta with one warning. A quad's front face is `+Z`, so a\nmonitor built from `Scene.quad` has to be rotated to face the viewer or the\nfeed reads mirrored."
        },
        {
          "name": "opacity",
          "qualified_name": "Scene.opacity",
          "kind": "value",
          "declaration": "let opacity : (float, t) => t",
          "docs": "Make a subtree TRANSLUCENT — alpha from 0 (invisible) to 1 (unchanged); the\nscene is last for piping.\n\nThe alpha applies uniformly to everything below it, whatever material each\nnode uses — solid colors, textures, lit models, terrain — so a ghost copy of\na craft is `craftScene(...) |> Scene.opacity(0.35)`. Nested opacities\nmultiply. An alpha outside `0..1` is an error, not a clamp.\n\nHOW IT RENDERS, so the caveats are predictable:\n\n- Translucent subtrees draw in a pass AFTER all opaque geometry, with depth\n  testing on and depth WRITING off — opaque things in front hide them; they\n  never hide each other.\n- They are sorted back-to-front by the VIEW-SPACE depth of the average world\n  position of the leaves under each `Scene.opacity` node. That is the whole\n  sorting granularity:\n  there is no per-triangle sort, so within ONE translucent subtree\n  overlapping surfaces (including the back faces of a closed mesh) read\n  denser where they overlap, and two INTERPENETRATING translucent objects\n  sort by their averages, which is wrong for the overlapping sliver.\n- A translucent subtree casts no shadow.\n- `Scene.opacity(1.0, scene)` is exactly `scene` — the identity. Nothing\n  that never calls this changes in cost or appearance. The flip side is a\n  STEP at that boundary: the instant alpha drops below 1 the subtree leaves\n  the opaque pass, stops writing depth AND stops casting a shadow, so a fade\n  beginning at exactly 1.0 pops on its first frame rather than easing.\n- An alpha of 0 draws nothing at all — the subtree is skipped rather than\n  rasterized — so fading fully out costs nothing."
        },
        {
          "name": "instanced",
          "qualified_name": "Scene.instanced",
          "kind": "value",
          "declaration": "let instanced : (List<Instance.t>, t) => t",
          "docs": "Stamp a scene once per instance — one node for thousands of copies.\n\n`scene |> Scene.instanced(instances)` is semantically a group holding one\ntransformed copy of the template per `Instance.t`, with each copy's\n`Instance.tint` multiplied into the template's material colors. Materials\ncome FROM the template: `Scene.cube() |> Scene.lit(color)` instanced is\nlit and shadowed exactly like its copies would be.\n\nThe renderer draws recognized templates with hardware instancing — a\nsingle cube/sphere/cylinder/quad/plane leaf under any transforms and at\nmost one solid `Scene.color` / `Scene.lit` / `Scene.emissive` material\n(one draw call), or a `Scene.model` leaf under transforms (one draw call\nper mesh primitive, textured exactly like the ordinary model draw). A\nSKINNED model template — with an attached `Scene.animate` pose or the\nzero-config first-clip autoplay — instances at the SHARED pose: the pose\nis sampled and uploaded once, and every copy skins from it (a crowd in\nstep; per-instance playheads are planned, not yet available). Any other\ntemplate still renders correctly, expanded copy-by-copy on the CPU with\na once-per-topology `[functor]` perf note — comparable to writing the\ngroup by hand, but not faster.\n\nAn empty list draws nothing. `Scene.opacity` inside the template is a\nteaching error — wrap the whole instanced node instead\n(`… |> Scene.instanced(xs) |> Scene.opacity(a)`)."
        },
        {
          "name": "animate",
          "qualified_name": "Scene.animate",
          "kind": "value",
          "declaration": "let animate : (Anim.t, t) => t",
          "docs": "Attach an animation pose to model nodes; the scene is last for piping.\n\nWithout an attached pose, a skinned model plays its FIRST clip on the game\nclock — attaching one is what puts the playhead under the game's control."
        },
        {
          "name": "translate",
          "qualified_name": "Scene.translate",
          "kind": "value",
          "declaration": "let translate : (Vec3.t, t) => t",
          "docs": "Translate a scene node; the scene is last for piping."
        },
        {
          "name": "scale",
          "qualified_name": "Scene.scale",
          "kind": "value",
          "declaration": "let scale : (float, t) => t",
          "docs": "Scale a scene node uniformly; the scene is last for piping."
        },
        {
          "name": "scaleXYZ",
          "qualified_name": "Scene.scaleXYZ",
          "kind": "value",
          "declaration": "let scaleXYZ : (float, float, float, t) => t",
          "docs": "Scale a scene node independently on each axis."
        },
        {
          "name": "rotateX",
          "qualified_name": "Scene.rotateX",
          "kind": "value",
          "declaration": "let rotateX : (Angle.t, t) => t",
          "docs": "Rotate a scene node around X."
        },
        {
          "name": "rotateY",
          "qualified_name": "Scene.rotateY",
          "kind": "value",
          "declaration": "let rotateY : (Angle.t, t) => t",
          "docs": "Rotate a scene node around Y."
        },
        {
          "name": "rotateZ",
          "qualified_name": "Scene.rotateZ",
          "kind": "value",
          "declaration": "let rotateZ : (Angle.t, t) => t",
          "docs": "Rotate a scene node around Z."
        },
        {
          "name": "equals",
          "qualified_name": "Scene.equals",
          "kind": "value",
          "declaration": "let equals : (t, t) => bool",
          "docs": "Compare two scene nodes structurally — the escape hatch for `Scene.t`,\nwhich is opaque and therefore supports no `==`.\n\nIntended for inline `expect` tests over `draw` output, NOT for per-frame\nlogic: the walk is O(scene size), which is why it is an explicit call\nrather than an operator.\n\nThree things it is literal about:\n\n- Floats compare EXACTLY — transforms, colors, and playheads. There is no\n  epsilon, so build both sides of a test from the same arithmetic. (The\n  comparison happens after the engine boundary's narrowing to 32-bit, so\n  two numbers that narrow to the same float are equal.)\n- Assets compare by LOCATOR, not by loaded content: the same path or URL\n  (and the same `Asset.whilePending` chain) is equal, whether or not either\n  has finished loading.\n- Animation compares as DECLARED — the clip name and playhead seconds in\n  the attached pose, never a sampled skeleton.\n\nChildren are ordered, so two groups holding the same nodes in a different\norder are not equal.\n\nThe answer is a bare `bool`, so a failing `expect` reports only that the\ntwo scenes differ — scene values are opaque and cannot be printed. Write\nthe assertion over the smallest node that makes the point, or bisect by\ncomparing sub-scenes, rather than one `expect` over a whole frame."
        }
      ]
    },
    {
      "name": "Instance",
      "group": "engine",
      "category": "Scene & rendering",
      "docs": "Per-copy placement for `Scene.instanced`.\n\nBuild one `Instance.t` per copy — where it sits, how it is scaled and\nrotated, and an optional color tint — then hand the list to\n`Scene.instanced`, which stamps its template scene once per instance.\n\nInstances are CHANNELS, not free-form transform chains: the copy's\ntransform always applies scale, then rotation, then translation, whatever\norder the combinators were piped in. Combinators compose within their own\nchannel — rotations multiply (the outer pipe applies last, like\n`Scene.rotate*`), scales multiply componentwise, and tints multiply\ncomponentwise.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Instance.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "One copy's placement: position, rotation, per-axis scale, and tint."
        },
        {
          "name": "at",
          "qualified_name": "Instance.at",
          "kind": "value",
          "declaration": "let at : (Vec3.t) => t",
          "docs": "Start an instance at `position`, with no rotation, unit scale, and no\ntint."
        },
        {
          "name": "scale",
          "qualified_name": "Instance.scale",
          "kind": "value",
          "declaration": "let scale : (float, t) => t",
          "docs": "Multiply the instance's scale uniformly."
        },
        {
          "name": "scaleXYZ",
          "qualified_name": "Instance.scaleXYZ",
          "kind": "value",
          "declaration": "let scaleXYZ : (float, float, float, t) => t",
          "docs": "Multiply the instance's per-axis scale componentwise."
        },
        {
          "name": "rotateX",
          "qualified_name": "Instance.rotateX",
          "kind": "value",
          "declaration": "let rotateX : (Angle.t, t) => t",
          "docs": "Rotate the instance about the X axis; piped later means applied later."
        },
        {
          "name": "rotateY",
          "qualified_name": "Instance.rotateY",
          "kind": "value",
          "declaration": "let rotateY : (Angle.t, t) => t",
          "docs": "Rotate the instance about the Y axis; piped later means applied later."
        },
        {
          "name": "rotateZ",
          "qualified_name": "Instance.rotateZ",
          "kind": "value",
          "declaration": "let rotateZ : (Angle.t, t) => t",
          "docs": "Rotate the instance about the Z axis; piped later means applied later."
        },
        {
          "name": "trs",
          "qualified_name": "Instance.trs",
          "kind": "value",
          "declaration": "let trs : (Vec3.t, Angle.t, float, float, float) => t",
          "docs": "The flat fast path: position, yaw, and per-axis scale in ONE call.\n\nExactly `Instance.at(position) |> Instance.scaleXYZ(sx, sy, sz)\n|> Instance.rotateY(rotationY)`, minus the per-copy builder-call overhead —\nreach for it when a large per-frame field makes construction cost visible.\nCompose further channels (another rotation axis, `Instance.tint`) on top."
        },
        {
          "name": "tint",
          "qualified_name": "Instance.tint",
          "kind": "value",
          "declaration": "let tint : (Color.t, t) => t",
          "docs": "Multiply the copy's material colors by `color` — a per-instance tint over\nthe template's material, not a second material system. Tints compose by\nmultiplying; white is the identity. The tint is RGB only — a copy's alpha\nalways comes from the template's material. A bare `Scene.model` template\nhas no material colors to multiply, so tint has no effect there (exactly\nas in the stamped group)."
        }
      ]
    },
    {
      "name": "Frame",
      "group": "engine",
      "category": "Scene & rendering",
      "docs": "Complete frame descriptions returned by a game's `draw` function.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Frame.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque frame description."
        },
        {
          "name": "create",
          "qualified_name": "Frame.create",
          "kind": "value",
          "declaration": "let create : (Camera3D.t, Scene.t) => t",
          "docs": "Create an unlit frame from a camera and scene."
        },
        {
          "name": "createLit",
          "qualified_name": "Frame.createLit",
          "kind": "value",
          "declaration": "let createLit : (Camera3D.t, Scene.t, List<Light.t>) => t",
          "docs": "Create a lit frame from a camera, scene, and lights."
        },
        {
          "name": "create2D",
          "qualified_name": "Frame.create2D",
          "kind": "value",
          "declaration": "let create2D : (Camera2D.t, Sprite.t) => t",
          "docs": "Create a standalone 2D frame from a camera and sprite tree."
        },
        {
          "name": "withRenderTarget",
          "qualified_name": "Frame.withRenderTarget",
          "kind": "value",
          "declaration": "let withRenderTarget : (RenderTarget.t, t, t) => t",
          "docs": "Render another frame into a target before rendering the main frame.\n\nThe target frame is a complete `Frame.create` / `Frame.createLit` with its\nOWN lights, so a lit or shadowed feed needs `createLit` plus\n`Light.castShadows` there. A scene that samples the very target it is drawn\ninto sees the previous frame's image. The main frame is last for piping."
        },
        {
          "name": "withUiTarget",
          "qualified_name": "Frame.withUiTarget",
          "kind": "value",
          "declaration": "let withUiTarget : (RenderTarget.t, Ui.view, t) => t",
          "docs": "Paint a UI view into a target before rendering the main frame.\n\nThe view is a `Ui.*` tree painted at the target's declared size and read\nback like any render target (`Scene.quad() |> Scene.screen(target)`) — a\nmonitor mesh, a cockpit panel. Views on targets are display-only for now:\ninteractive widgets (buttons, sliders, text inputs) render, but their\nhandlers are ignored. The screen clears to the engine's default\nbackground. Like `withRenderTarget`, the FIRST declaration of a target id\nwins; one id must not be declared by both writers. Runs on every shell —\nnative, web, and VR. The main frame is last for piping."
        },
        {
          "name": "withFog",
          "qualified_name": "Frame.withFog",
          "kind": "value",
          "declaration": "let withFog : (Fog.t, t) => t",
          "docs": "Attach fog to a frame; the frame is last for piping."
        },
        {
          "name": "withSkybox",
          "qualified_name": "Frame.withSkybox",
          "kind": "value",
          "declaration": "let withSkybox : (Skybox.t, t) => t",
          "docs": "Attach a cubemap skybox to a frame; the frame is last for piping."
        },
        {
          "name": "withClearColor",
          "qualified_name": "Frame.withClearColor",
          "kind": "value",
          "declaration": "let withClearColor : (Color.t, t) => t",
          "docs": "Override a frame's background clear color.\n\nThis overrides the default of clearing to the fog color, and paints the\nbackground only — it does not change how fog blends over geometry."
        },
        {
          "name": "with2D",
          "qualified_name": "Frame.with2D",
          "kind": "value",
          "declaration": "let with2D : (Camera2D.t, Sprite.t, t) => t",
          "docs": "Add an ordered sprite pass above the frame's 3D scene.\n\nLayers render in call order, so a second `with2D` draws above the first.\nThe main frame is last for piping."
        },
        {
          "name": "equals",
          "qualified_name": "Frame.equals",
          "kind": "value",
          "declaration": "let equals : (t, t) => bool",
          "docs": "Compare two frames structurally — the escape hatch for `Frame.t`, which is\nopaque and therefore supports no `==`.\n\nCompares every part of the frame: camera, scene, lights, render-target\npasses, fog, skybox, clear color, and 2D layers (all ordered). It also\ndistinguishes HOW the frame was built — a `Frame.create2D` frame is never\nequal to a 3D frame carrying the same layer through `with2D`. Intended for\ninline `expect` tests over `draw` output, NOT for per-frame logic — the\nwalk is O(frame size).\n\nIt inherits `Scene.equals`'s rules: floats compare exactly, assets compare\nby locator rather than by loaded content, animation compares as declared,\nand a failing `expect` can only report THAT the two frames differ — assert\nover the smallest piece that makes the point."
        }
      ]
    },
    {
      "name": "Camera3D",
      "group": "engine",
      "category": "Scene & rendering",
      "docs": "Cameras for viewing a Y-up, right-handed 3D scene.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Camera3D.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque camera description."
        },
        {
          "name": "lookAt",
          "qualified_name": "Camera3D.lookAt",
          "kind": "value",
          "declaration": "let lookAt : (Vec3.t, Vec3.t) => t",
          "docs": "Create a camera from an eye position and target with a fixed 45° field of view."
        },
        {
          "name": "firstPerson",
          "qualified_name": "Camera3D.firstPerson",
          "kind": "value",
          "declaration": "let firstPerson : (Vec3.t, Angle.t, Angle.t, Angle.t) => t",
          "docs": "Create a first-person camera from position, yaw, pitch, and field of view.\n\nZero yaw and pitch look down `+Z`.\n\nOn XR this camera is the authored reference center-eye rig: live head and\neye deltas compose in its local basis. Position, orientation, and the near/\nfar planes stay game-owned, while OpenXR owns IPD and per-eye optical FOV."
        },
        {
          "name": "ray",
          "qualified_name": "Camera3D.ray",
          "kind": "type",
          "declaration": "type ray = { origin: Vec3.t, direction: Vec3.t }",
          "docs": "A world-space ray from the camera eye through a logical surface point."
        },
        {
          "name": "toWorldRay",
          "qualified_name": "Camera3D.toWorldRay",
          "kind": "value",
          "declaration": "let toWorldRay : (Input.mouse, t) => Option.t<ray>",
          "docs": "Map a sampled mouse position through the authored perspective camera.\n\nMouse position and extent share one top-left-origin logical coordinate\nspace, so the result stays stable across resize and Retina/device-pixel\nratio changes. The direction is normalized and both fields feed directly\ninto `Physics.cast` / `Physics.raycast`. Returns `Option.None` while the\npointer is outside the surface or the camera is degenerate."
        },
        {
          "name": "mappedPose",
          "qualified_name": "Camera3D.mappedPose",
          "kind": "type",
          "declaration": "type mappedPose = { position: Input.point3, forward: Input.point3, up: Input.point3 }",
          "docs": "A tracked pose mapped into world space through an authored camera."
        },
        {
          "name": "mapTrackedPose",
          "qualified_name": "Camera3D.mapTrackedPose",
          "kind": "value",
          "declaration": "let mapTrackedPose : (t, Input.pose) => mappedPose",
          "docs": "Map a rig-local tracked pose through the authored camera.\n\nThe returned position, forward, and up vectors are suitable for placing a\ncontroller representation or aiming a ray in the authored world."
        },
        {
          "name": "clip",
          "qualified_name": "Camera3D.clip",
          "kind": "value",
          "declaration": "let clip : (float, float, t) => t",
          "docs": "Set near and far clipping distances; the camera is last for piping.\n\nBoth distances must be finite with `0 < near < far`; anything else is a\nteaching error rather than a degenerate projection. Large outdoor worlds\nshould set the far plane explicitly. Keep the near plane as large as\ngameplay permits to preserve depth-buffer precision."
        }
      ]
    },
    {
      "name": "Camera2D",
      "group": "engine",
      "category": "Scene & rendering",
      "docs": "Center-origin, Y-up cameras for `Sprite` scenes.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Camera2D.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque 2D camera description."
        },
        {
          "name": "create",
          "qualified_name": "Camera2D.create",
          "kind": "value",
          "declaration": "let create : (float, float) => t",
          "docs": "Create a camera with the visible world width and height at zoom 1.\n\nThe renderer preserves this aspect ratio and letterboxes rather than\nstretching. Width and height must be positive."
        },
        {
          "name": "at",
          "qualified_name": "Camera2D.at",
          "kind": "value",
          "declaration": "let at : (float, float, t) => t",
          "docs": "Center a camera at the given world position; the camera is last for piping."
        },
        {
          "name": "zoom",
          "qualified_name": "Camera2D.zoom",
          "kind": "value",
          "declaration": "let zoom : (float, t) => t",
          "docs": "Set a positive camera zoom; the camera is last for piping.\n\nLARGER is closer: the zoom divides the visible world extent, so `2.0` shows\nhalf as much world at twice the size."
        },
        {
          "name": "toWorld",
          "qualified_name": "Camera2D.toWorld",
          "kind": "value",
          "declaration": "let toWorld : (Input.mouse, t) => Option.t<Input.point2>",
          "docs": "Map a sampled mouse position through the camera's fitted viewport.\n\nReturns `Option.None` while the pointer is in a letterbox/pillarbox bar.\nThe mouse carries its logical surface extent, so this remains correct\nacross window resizes and Retina/device-pixel-ratio changes."
        }
      ]
    },
    {
      "name": "Sprite",
      "group": "engine",
      "category": "Scene & rendering",
      "docs": "Pure, inspectable 2D picture values.\n\nUnlike `Scene.t` and `Frame.t`, `Sprite.t` is represented at runtime as\nordinary Functor Lang data. It can be compared, inspected, serialized,\nstored in a model, and carried through time travel while its internal\nrendering schema stays private.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Sprite.t",
          "kind": "type",
          "declaration": "type t",
          "docs": "An abstract plain-data 2D picture."
        },
        {
          "name": "region",
          "qualified_name": "Sprite.region",
          "kind": "type",
          "declaration": "type region",
          "docs": "A rectangular section of an image in whole source pixels. Coordinates use\nthe conventional image origin: x grows right and y grows down from the\ntop-left corner."
        },
        {
          "name": "metrics",
          "qualified_name": "Sprite.metrics",
          "kind": "type",
          "declaration": "type metrics = {\n  width: float,\n  height: float\n}",
          "docs": "The size of a laid-out run of text, in the same world units as the sprite\nitself."
        },
        {
          "name": "blank",
          "qualified_name": "Sprite.blank",
          "kind": "value",
          "declaration": "let blank : () => t",
          "docs": "Create an empty picture."
        },
        {
          "name": "rectangle",
          "qualified_name": "Sprite.rectangle",
          "kind": "value",
          "declaration": "let rectangle : (Color.t, float, float) => t",
          "docs": "Create a centered rectangle with positive width and height."
        },
        {
          "name": "square",
          "qualified_name": "Sprite.square",
          "kind": "value",
          "declaration": "let square : (Color.t, float) => t",
          "docs": "Create a centered square with a positive side length."
        },
        {
          "name": "text",
          "qualified_name": "Sprite.text",
          "kind": "value",
          "declaration": "let text : (Color.t, float, string) => t",
          "docs": "Draw text in the built-in font, centered on its own box like every other\nprimitive, with `size` the height of one line in world units. Needs no\nasset: the font is compiled into the runtime. The text comes last, so a\nformatted value pipes straight in:\n\n    Text.fixed(model.score, 0.0) |> Sprite.text(Color.rgb(0.0, 1.0, 1.0), 1.2)\n\nThe font is a monospace 8x8 bitmap, so each character advances by exactly\n`size` — ask `measure` for a run's size rather than assuming it. `\\n` starts\na new line, stacked at exactly one `size` of line height, and each line is\ncentered within the block. That stride is the glyph cell, so lines never\noverlap but sit tight — a descender nearly meets the next line's capitals,\nas in a terminal. For airier text, draw the lines yourself and space them by\nmore than `size` with `group` and `moveY`. Characters outside printable ASCII\noccupy their cell but draw nothing, so unsupported text leaves gaps instead\nof shifting the rest of the line.\n\nText is centered, so align it by shifting half its measured width —\n`Sprite.moveX(Sprite.measure(size, s).width * 0.5, …)` puts its LEFT edge at\nthe origin, and negating that puts its right edge there. That aligns the\nBLOCK: with several lines only the widest reaches the edge, since each line\nstays centered until a left-aligning text block exists.\n\nGlyphs are sampled like any other sprite image, so `nearest` gives crisp\npixel edges and the default `linear` gives smoother ones at large sizes."
        },
        {
          "name": "measure",
          "qualified_name": "Sprite.measure",
          "kind": "value",
          "declaration": "let measure : (float, string) => metrics",
          "docs": "Measure what `text` at `size` would occupy, without rendering it, so labels\nand columns can be laid out in game logic. The width is the widest line's;\nthe height is `size` per line, counting a trailing newline as a line, so\nstacking blocks by their measured height never overlaps them."
        },
        {
          "name": "circle",
          "qualified_name": "Sprite.circle",
          "kind": "value",
          "declaration": "let circle : (Color.t, float) => t",
          "docs": "Create a filled circle of the given radius, centered on the origin like\n`square` — so it spans `2 * radius` across. Approximated by a 32-sided\npolygon, which is under a pixel from true at any size that reads as a circle."
        },
        {
          "name": "polygon",
          "qualified_name": "Sprite.polygon",
          "kind": "value",
          "declaration": "let polygon : (Color.t, List<Input.point2>) => t",
          "docs": "Fill a CONVEX polygon through the given points, in order.\n\nUnlike every other primitive, a polygon is NOT re-centered: the points are the\ngeometry, in the sprite's own coordinate space, so an outline computed in game\nlogic lands where it was computed. Either winding works (clockwise or\ncounter-clockwise).\n\nThe fill is a triangle fan, which is only correct for a convex outline, so\nanything else is REJECTED with an error rather than filled wrongly — draw it\nas a group of convex pieces instead. That covers a CONCAVE outline, a\nself-intersecting STAR (which turns consistently but winds around more than\nonce), fewer than 3 points, and points that are all on one line."
        },
        {
          "name": "line",
          "qualified_name": "Sprite.line",
          "kind": "value",
          "declaration": "let line : (Color.t, float, Input.point2, Input.point2) => t",
          "docs": "Draw a straight line of the given thickness between two points, in the\nsprite's own coordinate space (not re-centered, like `polygon`).\n\nThickness is measured across the line and is exact at every angle. There are\nno caps and no joins: the line stops flat at each endpoint, so two lines\nmeeting at an angle leave a notch at the corner — a jointed `polyline` is not\npart of this surface yet. A zero-length line draws nothing.\n\nThickness is geometry, not a screen-space stroke: `scale` multiplies it along\nwith the length, and `scaleXY` with unequal factors distorts it for any line\nthat is not axis-aligned."
        },
        {
          "name": "image",
          "qualified_name": "Sprite.image",
          "kind": "value",
          "declaration": "let image : (float, float, Asset.Texture) => t",
          "docs": "Create a centered, textured rectangle with positive width and height."
        },
        {
          "name": "imageRegion",
          "qualified_name": "Sprite.imageRegion",
          "kind": "value",
          "declaration": "let imageRegion : (float, float, region, Asset.Texture) => t",
          "docs": "Select a source rectangle without requiring the image's full dimensions."
        },
        {
          "name": "region",
          "qualified_name": "Sprite.region",
          "kind": "value",
          "declaration": "let region : (float, float, float, float) => region",
          "docs": "Construct a top-left-origin source rectangle as x, y, width, height."
        },
        {
          "name": "group",
          "qualified_name": "Sprite.group",
          "kind": "value",
          "declaration": "let group : (List<t>) => t",
          "docs": "Group pictures in painter's order, with earlier items behind later items."
        },
        {
          "name": "move",
          "qualified_name": "Sprite.move",
          "kind": "value",
          "declaration": "let move : (float, float, t) => t",
          "docs": "Move a picture by the given X and Y offsets; the picture is last for piping."
        },
        {
          "name": "moveX",
          "qualified_name": "Sprite.moveX",
          "kind": "value",
          "declaration": "let moveX : (float, t) => t",
          "docs": "Move a picture along X; the picture is last for piping."
        },
        {
          "name": "moveY",
          "qualified_name": "Sprite.moveY",
          "kind": "value",
          "declaration": "let moveY : (float, t) => t",
          "docs": "Move a picture along Y; the picture is last for piping."
        },
        {
          "name": "rotate",
          "qualified_name": "Sprite.rotate",
          "kind": "value",
          "declaration": "let rotate : (Angle.t, t) => t",
          "docs": "Rotate a picture around its center; the picture is last for piping.\n\nA positive angle rotates COUNTER-CLOCKWISE, matching the Y-up 2D camera."
        },
        {
          "name": "scale",
          "qualified_name": "Sprite.scale",
          "kind": "value",
          "declaration": "let scale : (float, t) => t",
          "docs": "Scale a picture uniformly; the picture is last for piping."
        },
        {
          "name": "scaleXY",
          "qualified_name": "Sprite.scaleXY",
          "kind": "value",
          "declaration": "let scaleXY : (float, float, t) => t",
          "docs": "Scale a picture independently along X and Y; the picture is last for piping."
        },
        {
          "name": "fade",
          "qualified_name": "Sprite.fade",
          "kind": "value",
          "declaration": "let fade : (float, t) => t",
          "docs": "Multiply a picture's opacity by an alpha from 0 to 1."
        },
        {
          "name": "tint",
          "qualified_name": "Sprite.tint",
          "kind": "value",
          "declaration": "let tint : (Color.t, t) => t",
          "docs": "Multiply a picture's color by a tint; the picture is last for piping."
        },
        {
          "name": "nearest",
          "qualified_name": "Sprite.nearest",
          "kind": "value",
          "declaration": "let nearest : (t) => t",
          "docs": "Use crisp nearest-neighbor sampling for every image in the subtree."
        },
        {
          "name": "linear",
          "qualified_name": "Sprite.linear",
          "kind": "value",
          "declaration": "let linear : (t) => t",
          "docs": "Use smooth linear sampling for every image in the subtree (the default)."
        }
      ]
    },
    {
      "name": "Light",
      "group": "engine",
      "category": "Scene & rendering",
      "docs": "Lights used by `Frame.createLit`.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Light.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque light description."
        },
        {
          "name": "ambient",
          "qualified_name": "Light.ambient",
          "kind": "value",
          "declaration": "let ambient : (Color.t) => t",
          "docs": "Create uniform ambient light."
        },
        {
          "name": "directional",
          "qualified_name": "Light.directional",
          "kind": "value",
          "declaration": "let directional : (Vec3.t, Color.t, float) => t",
          "docs": "Create directional light from direction, color, and intensity."
        },
        {
          "name": "point",
          "qualified_name": "Light.point",
          "kind": "value",
          "declaration": "let point : (Vec3.t, Color.t, float, float) => t",
          "docs": "Create point light from position, color, intensity, and range."
        },
        {
          "name": "spot",
          "qualified_name": "Light.spot",
          "kind": "value",
          "declaration": "let spot : (Vec3.t, Vec3.t, Color.t, float, float, Angle.t) => t",
          "docs": "Create a spot light from position, direction, color, intensity, range, and cone angle."
        },
        {
          "name": "castShadows",
          "qualified_name": "Light.castShadows",
          "kind": "value",
          "declaration": "let castShadows : (t) => t",
          "docs": "Enable shadow casting; the light is first for piping."
        }
      ]
    },
    {
      "name": "Skybox",
      "group": "engine",
      "category": "Scene & rendering",
      "docs": "Cubemap skyboxes attached to frames with `Frame.withSkybox`.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Skybox.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque cubemap skybox."
        },
        {
          "name": "files",
          "qualified_name": "Skybox.files",
          "kind": "value",
          "declaration": "let files : (string, string, string, string, string, string) => t",
          "docs": "Load six cubemap faces in `+X, -X, +Y, -Y, +Z, -Z` order.\n\nFaces are ordinary fetched image files resolved from the game directory.\nWhile they load the frame's clear color shows through, and a face that\nfails to load warns once and leaves the frame with no sky."
        }
      ]
    },
    {
      "name": "Texture",
      "group": "engine",
      "category": "Scene & rendering",
      "docs": "Texture values accepted by scene material functions.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Texture.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque texture value."
        },
        {
          "name": "file",
          "qualified_name": "Texture.file",
          "kind": "value",
          "declaration": "let file : (string) => t",
          "docs": "Load an image texture from a path relative to the game directory."
        }
      ]
    },
    {
      "name": "Fog",
      "group": "engine",
      "category": "Scene & rendering",
      "docs": "Distance fog attached to a frame with `Frame.withFog`.\n\nFog applies to every forward material, emissive included, and its color\nalso becomes the frame's clear color unless `Frame.withClearColor` says\notherwise. A skybox is never fogged.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Fog.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque fog description."
        },
        {
          "name": "linear",
          "qualified_name": "Fog.linear",
          "kind": "value",
          "declaration": "let linear : (float, float, Color.t) => t",
          "docs": "Create linear fog between near and far distances.\n\n`near` must be at least 0 and `far` must exceed `near`; anything else is a\nteaching error rather than a silently degenerate ramp."
        },
        {
          "name": "exp",
          "qualified_name": "Fog.exp",
          "kind": "value",
          "declaration": "let exp : (float, Color.t) => t",
          "docs": "Create exponential fog with the given density.\n\nThe density must be positive."
        }
      ]
    },
    {
      "name": "RenderTarget",
      "group": "engine",
      "category": "Scene & rendering",
      "docs": "Named off-screen render targets for render-to-texture effects.\n\nDeclare a target ONCE and use that value at both sites — the\n`Frame.withRenderTarget` writer and the `Scene.screen` reader — rather\nthan repeating a bare string, exactly as `Angle` and `Physics.tag` brand\ntheir own identities. A scene that samples the very target it is being\nrendered into shows the PREVIOUS frame's image.",
      "items": [
        {
          "name": "t",
          "qualified_name": "RenderTarget.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque render target identity and size."
        },
        {
          "name": "named",
          "qualified_name": "RenderTarget.named",
          "kind": "value",
          "declaration": "let named : (string) => t",
          "docs": "Create a stable, named 512×512 render target."
        },
        {
          "name": "sized",
          "qualified_name": "RenderTarget.sized",
          "kind": "value",
          "declaration": "let sized : (float, float, t) => t",
          "docs": "Set target width and height; the target is last for piping."
        }
      ]
    },
    {
      "name": "Vec3",
      "group": "engine",
      "category": "Math & geometry",
      "docs": "Branded 3D vectors used for positions, directions, velocities, and gravity.\n\nVectors are **opaque** — like `Angle` and `Color`, a `Vec3` is built once\n(`Vec3.make`) and passed as a value, so three interleaved bare floats can\nnever be mistaken for a position. Read components back with `Vec3.x` /\n`Vec3.y` / `Vec3.z`, and combine vectors with the arithmetic below rather\nthan unpacking to a record and rebuilding.\n\n**Argument order is thread-last**, matching the rest of the prelude\n(`Scene.translate(v, scene)`): the *subject* is the LAST parameter, so a\npipeline reads left-to-right as the subject being acted on.\n\n```\n// v - origin, scaled by 2, then normalized\nlet dir = v |> Vec3.sub(origin) |> Vec3.scale(2.0) |> Vec3.normalize()\n```\n\nFor the non-commutative operations this means `Vec3.sub(b, a)` computes\n`a - b`, so `v |> Vec3.sub(origin)` reads as \"v minus origin\"; likewise\n`a |> Vec3.cross(b)` is `a × b`, and `from |> Vec3.lerp(target, t)` moves\n`from` toward `target`.\n\nComponents are 32-bit floats and every vector is **finite**: an operation\nwhose result would overflow that range is an error naming the operation,\nrather than an infinity that becomes a NaN and silently blanks the scene.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Vec3.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque 3D vector."
        },
        {
          "name": "make",
          "qualified_name": "Vec3.make",
          "kind": "value",
          "declaration": "let make : (float, float, float) => t",
          "docs": "Construct a vector from X, Y, and Z components."
        },
        {
          "name": "x",
          "qualified_name": "Vec3.x",
          "kind": "value",
          "declaration": "let x : (t) => float",
          "docs": "The X component of a vector.\n\nComponents are stored as 32-bit floats, so a value that is not exactly\nrepresentable comes back rounded (`Vec3.x(Vec3.make(0.1, 0.0, 0.0))` is\n`0.10000000149011612`). Compare components with a tolerance, not `==`."
        },
        {
          "name": "y",
          "qualified_name": "Vec3.y",
          "kind": "value",
          "declaration": "let y : (t) => float",
          "docs": "The Y component of a vector. Rounded to 32-bit like `Vec3.x`."
        },
        {
          "name": "z",
          "qualified_name": "Vec3.z",
          "kind": "value",
          "declaration": "let z : (t) => float",
          "docs": "The Z component of a vector. Rounded to 32-bit like `Vec3.x`."
        },
        {
          "name": "add",
          "qualified_name": "Vec3.add",
          "kind": "value",
          "declaration": "let add : (t, t) => t",
          "docs": "Componentwise sum. `Vec3.add(b, a)` is `a + b`, so `a |> Vec3.add(b)`\nreads as \"a plus b\" (addition is commutative, so the order is free)."
        },
        {
          "name": "sub",
          "qualified_name": "Vec3.sub",
          "kind": "value",
          "declaration": "let sub : (t, t) => t",
          "docs": "Componentwise difference. `Vec3.sub(b, a)` is `a - b`, so\n`v |> Vec3.sub(origin)` reads as \"v minus origin\"."
        },
        {
          "name": "scale",
          "qualified_name": "Vec3.scale",
          "kind": "value",
          "declaration": "let scale : (float, t) => t",
          "docs": "Multiply every component by a scalar: `v |> Vec3.scale(2.0)` doubles `v`.\nScaling by a negative number negates: `v |> Vec3.scale(0.0 - 1.0)`."
        },
        {
          "name": "dot",
          "qualified_name": "Vec3.dot",
          "kind": "value",
          "declaration": "let dot : (t, t) => float",
          "docs": "The dot product `a · b` — commutative, so argument order does not matter.\nZero when the vectors are perpendicular."
        },
        {
          "name": "cross",
          "qualified_name": "Vec3.cross",
          "kind": "value",
          "declaration": "let cross : (t, t) => t",
          "docs": "The cross product. `Vec3.cross(b, a)` is `a × b`, so `a |> Vec3.cross(b)`\nreads as \"a cross b\" — the result is perpendicular to both, right-handed:\n`X × Y = Z`.\n\nMind the order for a strafe axis. In Functor's Y-up right-handed frame the\nworld-space \"right\" of a gaze is `up × forward`, which in this argument\norder is `up |> Vec3.cross(forward)` — with `forward = +Z` and `up = +Y`\nthat is `+X`. The other order gives `-X`."
        },
        {
          "name": "length",
          "qualified_name": "Vec3.length",
          "kind": "value",
          "declaration": "let length : (t) => float",
          "docs": "The Euclidean length (magnitude) of a vector."
        },
        {
          "name": "normalize",
          "qualified_name": "Vec3.normalize",
          "kind": "value",
          "declaration": "let normalize : (t) => t",
          "docs": "A unit vector pointing the same way.\n\n**A zero-length vector normalizes to zero**, not an error and not NaN —\n`Vec3.normalize(Vec3.make(0.0, 0.0, 0.0))` is the zero vector. Per-frame\ncode normalizes a velocity or an input direction that is legitimately\nzero all the time, so this must not fault the frame; test the length\nfirst when you need a fallback direction. Zero is the ONLY input that\nyields a non-unit result — the length is computed with enough range that\neven the largest representable vector normalizes correctly."
        },
        {
          "name": "distance",
          "qualified_name": "Vec3.distance",
          "kind": "value",
          "declaration": "let distance : (t, t) => float",
          "docs": "The distance between two points — symmetric, so argument order is free.\n`a |> Vec3.distance(b)` is the length of `a - b`."
        },
        {
          "name": "lerp",
          "qualified_name": "Vec3.lerp",
          "kind": "value",
          "declaration": "let lerp : (t, float, t) => t",
          "docs": "Linear interpolation. `Vec3.lerp(target, t, from)` moves `from` toward\n`target` by fraction `t`, so `from |> Vec3.lerp(target, 0.5)` is the\nmidpoint. `t` is NOT clamped: `0.0` yields `from`, `1.0` yields `target`,\nand values outside `0..1` extrapolate."
        }
      ]
    },
    {
      "name": "Angle",
      "group": "engine",
      "category": "Math & geometry",
      "docs": "Branded angles used by cameras, rotations, and spot lights.\n\nAngle-taking APIs require an `Angle.t`, preventing radians and degrees from\nbeing mixed accidentally. Construct one with `Angle.degrees` or\n`Angle.radians`.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Angle.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque angle value."
        },
        {
          "name": "degrees",
          "qualified_name": "Angle.degrees",
          "kind": "value",
          "declaration": "let degrees : (float) => t",
          "docs": "Construct an angle from degrees."
        },
        {
          "name": "radians",
          "qualified_name": "Angle.radians",
          "kind": "value",
          "declaration": "let radians : (float) => t",
          "docs": "Construct an angle from radians."
        },
        {
          "name": "add",
          "qualified_name": "Angle.add",
          "kind": "value",
          "declaration": "let add : (t, t) => t",
          "docs": "Add two angles. This is what `90deg + 45deg` calls."
        },
        {
          "name": "sub",
          "qualified_name": "Angle.sub",
          "kind": "value",
          "declaration": "let sub : (t, t) => t",
          "docs": "Subtract one angle from another. This is what `90deg - 45deg` calls."
        },
        {
          "name": "scale",
          "qualified_name": "Angle.scale",
          "kind": "value",
          "declaration": "let scale : (t, float) => t",
          "docs": "Scale an angle by a plain number. This is what `45deg * 2.0` calls.\nIt takes the angle FIRST — the shape every declared `*` has — so unlike\nmost of the prelude it is not written to be piped into."
        },
        {
          "name": "equals",
          "qualified_name": "Angle.equals",
          "kind": "value",
          "declaration": "let equals : (t, t) => bool",
          "docs": "Are two angles the same? This is what `90deg == 90deg` calls.\n\nIt is float equality on the underlying radians, with every consequence\nthat implies: `90deg == 90deg` is true because both sides build the same\nnumber, but `90deg == 1.5708rad` is false, and an angle accumulated\nthrough arithmetic may miss an exact literal by a rounding step. An angle\nis one-way (there is no way back to a number), so where a tolerance\nmatters, keep the plain float you built it from and compare THAT."
        },
        {
          "name": "less",
          "qualified_name": "Angle.less",
          "kind": "value",
          "declaration": "let less : (t, t) => bool",
          "docs": "Is the first angle smaller than the second? This is what `45deg < 90deg`\ncalls, and — swapped or negated — `>`, `<=`, and `>=` too.\n\nAngles are ordered by their raw radians, so this is signed magnitude, NOT\na direction on the circle: `-270deg < 90deg` is true."
        },
        {
          "name": "deg",
          "qualified_name": "Angle.deg",
          "kind": "unit",
          "declaration": "unit deg = Angle.degrees",
          "docs": "Degrees as a literal suffix: `90deg` is exactly `Angle.degrees(90.0)`."
        },
        {
          "name": "rad",
          "qualified_name": "Angle.rad",
          "kind": "unit",
          "declaration": "unit rad = Angle.radians",
          "docs": "Radians as a literal suffix: `0.5rad` is exactly `Angle.radians(0.5)`."
        },
        {
          "name": "deg (+)",
          "qualified_name": "Angle.deg (+)",
          "kind": "unit-operator",
          "declaration": "unit deg (+) = Angle.add",
          "docs": "`+` on angles: `90deg + 45deg` is `Angle.add(90deg, 45deg)`. An operator\nbelongs to the BRAND, so it covers every angle suffix — `90deg + 1.5rad`\nadds too."
        },
        {
          "name": "deg (-)",
          "qualified_name": "Angle.deg (-)",
          "kind": "unit-operator",
          "declaration": "unit deg (-) = Angle.sub",
          "docs": "`-` on angles: `90deg - 45deg` is `Angle.sub(90deg, 45deg)`."
        },
        {
          "name": "deg (*)",
          "qualified_name": "Angle.deg (*)",
          "kind": "unit-operator",
          "declaration": "unit deg (*) = Angle.scale",
          "docs": "`*` scales an angle by a number, on either side: `45deg * 2.0` and\n`2.0 * 45deg` are both `Angle.scale(45deg, 2.0)`. (Multiplying two angles\nwould be a different kind of thing — Functor Lang does not model that.)"
        },
        {
          "name": "deg (==)",
          "qualified_name": "Angle.deg (==)",
          "kind": "unit-operator",
          "declaration": "unit deg (==) = Angle.equals",
          "docs": "`==` on angles: `90deg == 90deg` is `Angle.equals(90deg, 90deg)`, and\n`!=` is its negation. Underneath it is FLOAT equality on radians — see\n`Angle.equals`."
        },
        {
          "name": "deg (<)",
          "qualified_name": "Angle.deg (<)",
          "kind": "unit-operator",
          "declaration": "unit deg (<) = Angle.less",
          "docs": "`<` on angles: `45deg < 90deg` is `Angle.less(45deg, 90deg)`. `>`, `<=`,\nand `>=` derive from it (swapped and/or negated), so all four orderings\ncome from this one declaration."
        }
      ]
    },
    {
      "name": "Color",
      "group": "engine",
      "category": "Math & geometry",
      "docs": "RGB colors shared by rendering, lighting, fog, and UI APIs.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Color.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque RGB color.\n\nChannels are normally in `0..1`; emissive and HDR uses may exceed `1`."
        },
        {
          "name": "rgb",
          "qualified_name": "Color.rgb",
          "kind": "value",
          "declaration": "let rgb : (float, float, float) => t",
          "docs": "Construct a color from red, green, and blue channels."
        }
      ]
    },
    {
      "name": "Physics",
      "group": "engine",
      "category": "Simulation",
      "docs": "Declarative rigid-body physics for the optional `physics` hook.\n\nShapes and bodies are values, body attributes pipe naturally, and stable\nbranded tags connect declarations, reads, commands, and collision events.\n\nThe hook DECLARES the world each frame and the runtime reconciles that\ndeclaration against the live one. A tag is cross-frame identity: the same\ntag is the same body, and a body is dropped by no longer declaring it.\nRe-declaring an unchanged body leaves the simulation alone, while changing\nits declared position or rotation drives that field — a dynamic or fixed\nbody teleports immediately, and a kinematic body takes the new pose as its\nnext-step target, so it carries velocity into contacts. Reading a tag that\nis not in the live world is a runtime error rather than an empty answer, so\nread only bodies your hook has declared. Like the model, the world survives\nhot reload for as long as the hook does; deleting the hook drops it. An\nerror raised inside the hook does NOT stop the world: the previous frame's\ndeclaration is kept and stepped, and the error is reported once.\n\nReads are SYNCHRONOUS and writes are QUEUED. `Physics.position`,\n`Physics.linearVelocity`, and `Physics.cast` answer in place from the LAST\nSTEPPED world — in any entry point, the `physics` hook included. Everything\nthat runs before the step (`tick`, `input`, a pre-step `update`, the hook)\nsees the previous step; only `draw` and the post-step `update`s see the\nworld this frame just stepped. The world is primed from `init` before the\nfirst frame, so frame 1's reads answer with the initial declared poses.\nEvery mutation instead returns an `Effect.t` that applies at the next\nphysics step after it queues — after reconcile, on that step's first\nsubstep. A command issued from `tick` therefore normally lands in time for\nthe same frame's `draw`; on a frame that takes no substep at all (normal\nabove 60fps) it waits for the next simulated frame.",
      "items": [
        {
          "name": "shape",
          "qualified_name": "Physics.shape",
          "kind": "type",
          "declaration": "type shape = host",
          "docs": "An opaque collision shape."
        },
        {
          "name": "body",
          "qualified_name": "Physics.body",
          "kind": "type",
          "declaration": "type body = host",
          "docs": "An opaque rigid body description."
        },
        {
          "name": "world",
          "qualified_name": "Physics.world",
          "kind": "type",
          "declaration": "type world = host",
          "docs": "An opaque physics world description."
        },
        {
          "name": "tag",
          "qualified_name": "Physics.tag",
          "kind": "type",
          "declaration": "type tag",
          "docs": "A stable, branded body identity used throughout the physics API."
        },
        {
          "name": "position",
          "qualified_name": "Physics.position",
          "kind": "type",
          "declaration": "type position = { x: float, y: float, z: float }",
          "docs": "The live world-space position of a body."
        },
        {
          "name": "velocity",
          "qualified_name": "Physics.velocity",
          "kind": "type",
          "declaration": "type velocity = { x: float, y: float, z: float }",
          "docs": "The live linear velocity of a body, in world units per second."
        },
        {
          "name": "rayHit",
          "qualified_name": "Physics.rayHit",
          "kind": "type",
          "declaration": "type rayHit = {\n  hit: bool,\n  x: float, y: float, z: float,\n  nx: float, ny: float, nz: float,\n  distance: float,\n  tag: tag\n}",
          "docs": "A raycast result with hit position, normal, distance, and body tag.\n\nFor a miss, `hit` is false and the remaining fields are zeroed."
        },
        {
          "name": "collisionEvent",
          "qualified_name": "Physics.collisionEvent",
          "kind": "type",
          "declaration": "type collisionEvent = { started: bool, a: tag, b: tag, sensor: bool }",
          "docs": "A contact-begin or contact-end event between two bodies."
        },
        {
          "name": "tag",
          "qualified_name": "Physics.tag",
          "kind": "value",
          "declaration": "let tag : (string) => tag",
          "docs": "Construct a stable body tag from a string.\n\nThe empty tag is reserved as the no-body sentinel in a raycast miss.\n\nThe brand is check-time only: declare a tag once and use that VALUE at\nevery site — a bare string where a tag is expected is a check error. At\nruntime a tag simply IS its string, so comparing one against a collision\nevent's `a`/`b` with `==` works."
        },
        {
          "name": "box",
          "qualified_name": "Physics.box",
          "kind": "value",
          "declaration": "let box : (float, float, float) => shape",
          "docs": "Create a local-axis box shape from its full width, height, and depth."
        },
        {
          "name": "sphere",
          "qualified_name": "Physics.sphere",
          "kind": "value",
          "declaration": "let sphere : (float) => shape",
          "docs": "Create a sphere shape from its radius."
        },
        {
          "name": "capsule",
          "qualified_name": "Physics.capsule",
          "kind": "value",
          "declaration": "let capsule : (float, float) => shape",
          "docs": "Create a capsule shape from half-height and radius."
        },
        {
          "name": "heightfield",
          "qualified_name": "Physics.heightfield",
          "kind": "value",
          "declaration": "let heightfield : (tag, Terrain.t) => body",
          "docs": "Create a fixed heightfield body from a shared terrain descriptor.\n\nThe renderer and collider share dimensions, elevation range, asset\nresolution, and pending-asset chain. Collision uses at most 1025 samples\nper axis to bound frame-thread work; larger render sources are decimated.\nThe body supports translation via `Physics.at`; pair it only with an\nunrotated, unscaled `Scene.terrain`, using the same translation."
        },
        {
          "name": "dynamic",
          "qualified_name": "Physics.dynamic",
          "kind": "value",
          "declaration": "let dynamic : (tag, shape) => body",
          "docs": "Create a dynamic body affected by forces and collisions."
        },
        {
          "name": "kinematic",
          "qualified_name": "Physics.kinematic",
          "kind": "value",
          "declaration": "let kinematic : (tag, shape) => body",
          "docs": "Create a kinematic body driven explicitly by the game."
        },
        {
          "name": "fixed",
          "qualified_name": "Physics.fixed",
          "kind": "value",
          "declaration": "let fixed : (tag, shape) => body",
          "docs": "Create a fixed body that does not move."
        },
        {
          "name": "at",
          "qualified_name": "Physics.at",
          "kind": "value",
          "declaration": "let at : (Vec3.t, body) => body",
          "docs": "Set a body's initial world position; the body is last for piping."
        },
        {
          "name": "rotateX",
          "qualified_name": "Physics.rotateX",
          "kind": "value",
          "declaration": "let rotateX : (Angle.t, body) => body",
          "docs": "Rotate a body about world X around its center; the body is last for piping.\n\nAn outer rotation applies last in world space, matching `Scene.rotateX`.\nHeightfield bodies reject rotation because terrain rendering is\ntranslation-only."
        },
        {
          "name": "rotateY",
          "qualified_name": "Physics.rotateY",
          "kind": "value",
          "declaration": "let rotateY : (Angle.t, body) => body",
          "docs": "Rotate a body about world Y around its center; the body is last for piping.\n\nAn outer rotation applies last in world space, matching `Scene.rotateY`.\nHeightfield bodies reject rotation because terrain rendering is\ntranslation-only."
        },
        {
          "name": "rotateZ",
          "qualified_name": "Physics.rotateZ",
          "kind": "value",
          "declaration": "let rotateZ : (Angle.t, body) => body",
          "docs": "Rotate a body about world Z around its center; the body is last for piping.\n\nAn outer rotation applies last in world space, matching `Scene.rotateZ`.\nHeightfield bodies reject rotation because terrain rendering is\ntranslation-only."
        },
        {
          "name": "velocity",
          "qualified_name": "Physics.velocity",
          "kind": "value",
          "declaration": "let velocity : (Vec3.t, body) => body",
          "docs": "Set a body's initial linear velocity; the body is last for piping."
        },
        {
          "name": "mass",
          "qualified_name": "Physics.mass",
          "kind": "value",
          "declaration": "let mass : (float, body) => body",
          "docs": "Set a body's mass; the body is last for piping."
        },
        {
          "name": "friction",
          "qualified_name": "Physics.friction",
          "kind": "value",
          "declaration": "let friction : (float, body) => body",
          "docs": "Set a body's friction coefficient; the body is last for piping."
        },
        {
          "name": "restitution",
          "qualified_name": "Physics.restitution",
          "kind": "value",
          "declaration": "let restitution : (float, body) => body",
          "docs": "Set a body's restitution; the body is last for piping."
        },
        {
          "name": "linearDamping",
          "qualified_name": "Physics.linearDamping",
          "kind": "value",
          "declaration": "let linearDamping : (float, body) => body",
          "docs": "Damp a body's LINEAR velocity — drag, per second; the body is last for piping.\n\nThe default is `0.0` (no drag), so a rolling sphere on a box coasts almost\nforever: contact friction resists sliding, not rolling. A small value\n(`0.3`–`0.8`) is what makes a marble, a puck, or a thrown prop actually\nsettle, and it belongs here rather than in a per-frame velocity command:\ndamping is a property of the body, so declaring it keeps `tick` pure.\nMust not be negative. Only `dynamic` bodies integrate, so damping is inert\non a `kinematic` or `fixed` one. Changing the value writes it onto the live\nbody (the friction/restitution rule) — the new drag applies from the next\nstep, with the body's current pose and velocity untouched."
        },
        {
          "name": "angularDamping",
          "qualified_name": "Physics.angularDamping",
          "kind": "value",
          "declaration": "let angularDamping : (float, body) => body",
          "docs": "Damp a body's ANGULAR velocity — spin resistance, per second; the body is\nlast for piping.\n\nThe default is `0.0`. Pair it with `linearDamping` for a ball that stops\nrolling instead of creeping, or use it alone to bleed off spin while linear\nmotion is preserved. Must not be negative, `dynamic`-only, and reconciles\nonto a live body exactly like `linearDamping`."
        },
        {
          "name": "sensor",
          "qualified_name": "Physics.sensor",
          "kind": "value",
          "declaration": "let sensor : (body) => body",
          "docs": "Make a body a non-solid sensor; the body is last for piping."
        },
        {
          "name": "upright",
          "qualified_name": "Physics.upright",
          "kind": "value",
          "declaration": "let upright : (body) => body",
          "docs": "Lock a body's rotation so it translates but never tips.\n\nThe character-controller attribute: an upright capsule that lands, scuffs a\nledge, or leans on a wall would otherwise pick up angular velocity and\ntopple, which also invalidates any fixed standing-height assumption a\ngrounding probe makes. The body is last for piping."
        },
        {
          "name": "scene",
          "qualified_name": "Physics.scene",
          "kind": "value",
          "declaration": "let scene : (Vec3.t, List<body>) => world",
          "docs": "Declare a physics world from gravity and bodies."
        },
        {
          "name": "position",
          "qualified_name": "Physics.position",
          "kind": "value",
          "declaration": "let position : (tag) => position",
          "docs": "Read a body's live, stepped world position.\n\nAnswers with the LAST stepped world, so a pre-step caller (`tick`, the\n`physics` hook) sees the previous step and `draw` sees this frame's."
        },
        {
          "name": "linearVelocity",
          "qualified_name": "Physics.linearVelocity",
          "kind": "value",
          "declaration": "let linearVelocity : (tag) => velocity",
          "docs": "Read a body's live, stepped linear velocity.\n\nThe read counterpart of `Physics.setVelocity`. (`Physics.velocity` is the\nbody-builder attribute that sets an *initial* velocity.)"
        },
        {
          "name": "cast",
          "qualified_name": "Physics.cast",
          "kind": "value",
          "declaration": "let cast : (Vec3.t, Vec3.t, float) => rayHit",
          "docs": "Cast a ray against the world and get the nearest hit immediately.\n\nUnlike `Physics.raycast` — an effect whose answer arrives through `update`\nafter the step — this answers in place, so `tick` can branch on it while\ndeciding. It reads the world as of the last step, like `Physics.position`:\nin `tick` that is the previous step, in `draw` this frame's. A miss is\n`hit: false` with zeroed fields, not an error. `dir` need not be\nnormalized, so `maxDist` is in world units."
        },
        {
          "name": "castExcluding",
          "qualified_name": "Physics.castExcluding",
          "kind": "value",
          "declaration": "let castExcluding : (tag, Vec3.t, Vec3.t, float) => rayHit",
          "docs": "`Physics.cast`, ignoring one body — the grounding probe.\n\nA ray cast from inside a character's own capsule would otherwise hit that\ncapsule at distance 0 and report the character standing on itself. Excluding\na tag that isn't in the world excludes nothing."
        },
        {
          "name": "transformed",
          "qualified_name": "Physics.transformed",
          "kind": "value",
          "declaration": "let transformed : (tag, Scene.t) => Scene.t",
          "docs": "Apply a body's live transform to a scene node.\n\nThe scene is last for piping."
        },
        {
          "name": "applyImpulse",
          "qualified_name": "Physics.applyImpulse",
          "kind": "value",
          "declaration": "let applyImpulse : (tag, Vec3.t) => Effect.t",
          "docs": "Apply an instantaneous impulse to a body."
        },
        {
          "name": "applyForce",
          "qualified_name": "Physics.applyForce",
          "kind": "value",
          "declaration": "let applyForce : (tag, Vec3.t) => Effect.t",
          "docs": "Apply a continuous force to a body for the next step."
        },
        {
          "name": "setVelocity",
          "qualified_name": "Physics.setVelocity",
          "kind": "value",
          "declaration": "let setVelocity : (tag, Vec3.t) => Effect.t",
          "docs": "Replace a body's linear velocity — all three axes.\n\nFor a character controller prefer `Physics.setVelocityXZ`: writing the\nvertical axis every frame fights the solver's own ground contact."
        },
        {
          "name": "setVelocityXZ",
          "qualified_name": "Physics.setVelocityXZ",
          "kind": "value",
          "declaration": "let setVelocityXZ : (tag, float, float) => Effect.t",
          "docs": "Replace a body's HORIZONTAL velocity, leaving the vertical axis alone.\n\nThe character-controller command. Steering owns x and z while the solver\nkeeps the y it is using to resolve the ground contact, so the game never\nhas to author a vertical velocity it has no opinion about — with\n`Physics.setVelocity` it must write all three every frame, and the only\nvalues available to it are a one-step-stale read or a guess.\n\nThe preserved axis is read from the live world when the command applies —\nafter reconcile, and after any command queued earlier the same frame. So\nvelocity commands in one frame compose as last-write-wins per axis, and an\naxis nobody wrote is left exactly as the solver left it."
        },
        {
          "name": "setVelocityY",
          "qualified_name": "Physics.setVelocityY",
          "kind": "value",
          "declaration": "let setVelocityY : (tag, float) => Effect.t",
          "docs": "Replace a body's VERTICAL velocity, leaving the horizontal plane alone.\n\nA jump that keeps the run: unlike `Physics.applyImpulse` the result does\nnot depend on the body's mass, and unlike `Physics.setVelocity` it does not\ndiscard the horizontal momentum the character arrived with."
        },
        {
          "name": "teleport",
          "qualified_name": "Physics.teleport",
          "kind": "value",
          "declaration": "let teleport : (tag, Vec3.t) => Effect.t",
          "docs": "Move a body immediately to a world position."
        },
        {
          "name": "raycast",
          "qualified_name": "Physics.raycast",
          "kind": "value",
          "declaration": "let raycast : (Vec3.t, Vec3.t, float, (rayHit) => 'msg) => Effect.t",
          "docs": "Cast a ray and tag its `Physics.rayHit` result as a message."
        },
        {
          "name": "events",
          "qualified_name": "Physics.events",
          "kind": "value",
          "declaration": "let events : ((collisionEvent) => 'msg) => Sub.t",
          "docs": "Subscribe to contact begin/end events and tag them as messages."
        }
      ]
    },
    {
      "name": "Anim",
      "group": "engine",
      "category": "Simulation",
      "docs": "Declarative animation poses attached to scene models with `Scene.animate`.\n\nPlayheads and blend weights are values derived by the game, so the engine\nowns no hidden animation clock and poses rewind and replay deterministically.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Anim.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque animation pose expression."
        },
        {
          "name": "clip",
          "qualified_name": "Anim.clip",
          "kind": "value",
          "declaration": "let clip : (string, float) => t",
          "docs": "Sample a named glTF clip at a playhead in seconds.\n\nThe clip loops by its duration; negative playheads wrap backwards from the\nend. A name the model does not define warns once and renders the bind pose\n— `functor import`'s generated clip constants (`Assets.xbotClips.walk.name`)\nturn that into a check-time error instead."
        },
        {
          "name": "blend",
          "qualified_name": "Anim.blend",
          "kind": "value",
          "declaration": "let blend : (List<(t, float)>) => t",
          "docs": "Blend a list of `(animation, weight)` pairs.\n\nWeights are normalized and entries with non-positive weights are ignored.\nTranslation and scale are interpolated linearly and rotation as a\nnormalized quaternion mix. An entry may itself be a blend, so blends nest."
        },
        {
          "name": "rest",
          "qualified_name": "Anim.rest",
          "kind": "value",
          "declaration": "let rest : () => t",
          "docs": "Return the bind pose as a base for programmatic posing."
        },
        {
          "name": "add",
          "qualified_name": "Anim.add",
          "kind": "value",
          "declaration": "let add : (t, float, t) => t",
          "docs": "Apply an additive animation layer to a base pose.\n\nThe base is last for piping. Weight is clamped to `0..1`, and the delta\napplies only where the base has influence."
        },
        {
          "name": "mask",
          "qualified_name": "Anim.mask",
          "kind": "value",
          "declaration": "let mask : (List<string>, t) => t",
          "docs": "Restrict a pose to the subtrees rooted at the named joints.\n\nJoints the mask does not cover fall out of this pose entirely — they take\nthe bind pose, or the other inputs of an enclosing blend. A joint name the\nmodel does not define warns once."
        },
        {
          "name": "rotate",
          "qualified_name": "Anim.rotate",
          "kind": "value",
          "declaration": "let rotate : (string, Angle.t, Angle.t, Angle.t, t) => t",
          "docs": "Add a local XYZ Euler rotation to one joint.\n\nThe joint counts as FULLY DRIVEN by this node, so a mask BENEATH it cannot\ndrop the joint; an enclosing mask — one applied to this node's result —\nstill can."
        },
        {
          "name": "lookAt",
          "qualified_name": "Anim.lookAt",
          "kind": "value",
          "declaration": "let lookAt : (string, Vec3.t, Angle.t, float, t) => t",
          "docs": "Aim one joint's local +Z axis at a model-space target after evaluating the\npose below it.\n\nThe target is baked into the animation value at draw time, and `Scene`\ntransforms sit deliberately outside the solver — so a world-space aim point\nhas to be inverted through the node's own transforms before it is passed\nhere. `maxDeflection` limits the shortest correction from the evaluated pose and\nmust be between 0 and 180 degrees. `weight` is clamped to `0..1`. The joint\nis fully driven by this node, exactly as with `Anim.rotate`; an enclosing\nmask can still exclude it."
        },
        {
          "name": "reach",
          "qualified_name": "Anim.reach",
          "kind": "value",
          "declaration": "let reach : (string, string, string, Vec3.t, float, t) => t",
          "docs": "Reach a model-space target with a direct two-bone joint chain.\n\n`root`, `middle`, and `end` must name direct parent/child joints, such as an\nupper arm, forearm, and hand. Unreachable targets clamp to the chain's\nnearest extension. The evaluated pose below supplies the elbow bend side,\n`root` must have uniform scale, and `weight` is clamped to `0..1`."
        }
      ]
    },
    {
      "name": "Terrain",
      "group": "engine",
      "category": "Simulation",
      "docs": "Finite, asset-backed heightfield terrain shared by rendering and physics.\n\nHeightmaps should be 16-bit grayscale PNGs. Black maps to the declared\nminimum height and white maps to the maximum height.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Terrain.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An immutable terrain descriptor."
        },
        {
          "name": "heightmap",
          "qualified_name": "Terrain.heightmap",
          "kind": "value",
          "declaration": "let heightmap : (Asset.Texture, float, float, float, float) => t",
          "docs": "Create a terrain centered on the origin and spanning `width` by `depth` in XZ."
        },
        {
          "name": "maxPixelError",
          "qualified_name": "Terrain.maxPixelError",
          "kind": "value",
          "declaration": "let maxPixelError : (float, t) => t",
          "docs": "Set the maximum projected vertex spacing in pixels; lower is more detailed.\n\nThe default is 2 pixels. The terrain is last for piping."
        },
        {
          "name": "color",
          "qualified_name": "Terrain.color",
          "kind": "value",
          "declaration": "let color : (Color.t, t) => t",
          "docs": "Set the basic lit terrain color; the terrain is last for piping.\n\nThis is the ALTERNATIVE to `Terrain.layered`, not a stage before it: it\nclears any layers, so whichever of the two comes last in a pipeline wins\nand the earlier one is simply dead. Pick one."
        },
        {
          "name": "layered",
          "qualified_name": "Terrain.layered",
          "kind": "value",
          "declaration": "let layered : (Color.t, Color.t, Color.t, Color.t, float, t) => t",
          "docs": "Blend lowland, highland, rock, and snow colors by height and slope.\n\n`snowHeight` is in terrain-local world units. The terrain is last for\npiping."
        },
        {
          "name": "textured",
          "qualified_name": "Terrain.textured",
          "kind": "value",
          "declaration": "let textured : (Asset.Texture, Asset.Texture, Asset.Texture, Asset.Texture, float, t) => t",
          "docs": "Dress the `layered` bands with detail maps.\n\nEach map supplies STRUCTURE, not color: it is divided by its own average, so\nit adds surface detail to the band's `layered` color without repainting it.\n(A photographic ground albedo averages brown; used directly it would turn a\ngreen hillside to dirt.) The maps blend by the same height and slope weights\nas the colors, so texturing changes what a band looks like, not where it\nfalls. `tileSize` is the world-unit span of one tile; each map is sampled at\ntwo scales to hide the repeat, and detail fades out with distance. Requires\n`layered`. The terrain is last for piping."
        },
        {
          "name": "grass",
          "qualified_name": "Terrain.grass",
          "kind": "value",
          "declaration": "let grass : (float, float, float, Color.t, t) => t",
          "docs": "Add camera-local GPU-instanced grass clusters.\n\n`spacing`, `distance`, and `bladeHeight` are terrain-local world units.\nGrass is suppressed on steep, low-lying, and snowy samples. The terrain is\nlast for piping."
        }
      ]
    },
    {
      "name": "Time",
      "group": "engine",
      "category": "Simulation",
      "docs": "Branded durations used by timing APIs such as `Sub.every`.\n\nConstructing a `Time.t` explicitly prevents milliseconds and seconds from\nbeing mixed accidentally.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Time.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque duration."
        },
        {
          "name": "seconds",
          "qualified_name": "Time.seconds",
          "kind": "value",
          "declaration": "let seconds : (float) => t",
          "docs": "Construct a duration from seconds."
        },
        {
          "name": "millis",
          "qualified_name": "Time.millis",
          "kind": "value",
          "declaration": "let millis : (float) => t",
          "docs": "Construct a duration from milliseconds."
        },
        {
          "name": "micros",
          "qualified_name": "Time.micros",
          "kind": "value",
          "declaration": "let micros : (float) => t",
          "docs": "Construct a duration from microseconds."
        },
        {
          "name": "minutes",
          "qualified_name": "Time.minutes",
          "kind": "value",
          "declaration": "let minutes : (float) => t",
          "docs": "Construct a duration from minutes."
        },
        {
          "name": "hours",
          "qualified_name": "Time.hours",
          "kind": "value",
          "declaration": "let hours : (float) => t",
          "docs": "Construct a duration from hours."
        },
        {
          "name": "add",
          "qualified_name": "Time.add",
          "kind": "value",
          "declaration": "let add : (t, t) => t",
          "docs": "Add two durations. This is what `1.5s + 200ms` calls."
        },
        {
          "name": "sub",
          "qualified_name": "Time.sub",
          "kind": "value",
          "declaration": "let sub : (t, t) => t",
          "docs": "Subtract one duration from another. This is what `1.5s - 200ms` calls."
        },
        {
          "name": "scale",
          "qualified_name": "Time.scale",
          "kind": "value",
          "declaration": "let scale : (t, float) => t",
          "docs": "Scale a duration by a plain number. This is what `0.5s * 2.0` calls.\nIt takes the duration FIRST — the shape every declared `*` has — so unlike\nmost of the prelude it is not written to be piped into."
        },
        {
          "name": "equals",
          "qualified_name": "Time.equals",
          "kind": "value",
          "declaration": "let equals : (t, t) => bool",
          "docs": "Are two durations the same? This is what `1s == 1000ms` calls.\n\nDurations are stored in seconds, so this is FLOAT equality on that number:\n`1s == 1000ms` is true, but a duration accumulated through arithmetic may\nmiss an exact literal by a rounding step. A duration is one-way (there is\nno way back to a number), so where a tolerance matters, keep the plain\nfloat you built it from and compare THAT."
        },
        {
          "name": "less",
          "qualified_name": "Time.less",
          "kind": "value",
          "declaration": "let less : (t, t) => bool",
          "docs": "Is the first duration shorter than the second? This is what\n`200ms < 1.5s` calls, and — swapped or negated — `>`, `<=`, and `>=` too."
        },
        {
          "name": "s",
          "qualified_name": "Time.s",
          "kind": "unit",
          "declaration": "unit s = Time.seconds",
          "docs": "Seconds as a literal suffix: `0.5s` is exactly `Time.seconds(0.5)`."
        },
        {
          "name": "ms",
          "qualified_name": "Time.ms",
          "kind": "unit",
          "declaration": "unit ms = Time.millis",
          "docs": "Milliseconds as a literal suffix: `500ms` is exactly `Time.millis(500.0)`."
        },
        {
          "name": "us",
          "qualified_name": "Time.us",
          "kind": "unit",
          "declaration": "unit us = Time.micros",
          "docs": "Microseconds as a literal suffix: `250us` is exactly `Time.micros(250.0)`."
        },
        {
          "name": "min",
          "qualified_name": "Time.min",
          "kind": "unit",
          "declaration": "unit min = Time.minutes",
          "docs": "Minutes as a literal suffix: `2min` is exactly `Time.minutes(2.0)`."
        },
        {
          "name": "hr",
          "qualified_name": "Time.hr",
          "kind": "unit",
          "declaration": "unit hr = Time.hours",
          "docs": "Hours as a literal suffix: `1hr` is exactly `Time.hours(1.0)`."
        },
        {
          "name": "s (+)",
          "qualified_name": "Time.s (+)",
          "kind": "unit-operator",
          "declaration": "unit s (+) = Time.add",
          "docs": "`+` on durations: `1.5s + 200ms` is `Time.add(1.5s, 200ms)`. An operator\nbelongs to the BRAND, so every duration suffix shares it — seconds and\nmilliseconds add directly."
        },
        {
          "name": "s (-)",
          "qualified_name": "Time.s (-)",
          "kind": "unit-operator",
          "declaration": "unit s (-) = Time.sub",
          "docs": "`-` on durations: `1.5s - 200ms` is `Time.sub(1.5s, 200ms)`."
        },
        {
          "name": "s (*)",
          "qualified_name": "Time.s (*)",
          "kind": "unit-operator",
          "declaration": "unit s (*) = Time.scale",
          "docs": "`*` scales a duration by a number, on either side: `0.5s * 2.0` and\n`2.0 * 0.5s` are both `Time.scale(0.5s, 2.0)`."
        },
        {
          "name": "s (==)",
          "qualified_name": "Time.s (==)",
          "kind": "unit-operator",
          "declaration": "unit s (==) = Time.equals",
          "docs": "`==` on durations: `1s == 1000ms` is `Time.equals(1s, 1000ms)`, and `!=`\nis its negation. Underneath it is FLOAT equality on seconds — see\n`Time.equals`."
        },
        {
          "name": "s (<)",
          "qualified_name": "Time.s (<)",
          "kind": "unit-operator",
          "declaration": "unit s (<) = Time.less",
          "docs": "`<` on durations: `200ms < 1.5s` is `Time.less(200ms, 1.5s)`. `>`, `<=`,\nand `>=` derive from it (swapped and/or negated), so all four orderings\ncome from this one declaration."
        }
      ]
    },
    {
      "name": "Input",
      "group": "engine",
      "category": "Input",
      "docs": "Target-neutral continuously sampled input.\n\nThe optional `sampledInput(model, snapshot)` game hook receives one\n`Input.snapshot` immediately before every fixed simulation step. XR,\ngamepad, and touch are typed device domains; further devices belong as\nsiblings beside them on the snapshot.",
      "items": [
        {
          "name": "point2",
          "qualified_name": "Input.point2",
          "kind": "type",
          "declaration": "type point2 = { x: float, y: float }",
          "docs": "A plain two-dimensional point or axis pair."
        },
        {
          "name": "point3",
          "qualified_name": "Input.point3",
          "kind": "type",
          "declaration": "type point3 = { x: float, y: float, z: float }",
          "docs": "A plain three-dimensional point or direction."
        },
        {
          "name": "quaternion",
          "qualified_name": "Input.quaternion",
          "kind": "type",
          "declaration": "type quaternion = { x: float, y: float, z: float, w: float }",
          "docs": "A quaternion in `[x, y, z, w]` component order, matching OpenXR and glTF."
        },
        {
          "name": "pose",
          "qualified_name": "Input.pose",
          "kind": "type",
          "declaration": "type pose = { position: point3, orientation: quaternion }",
          "docs": "A rig-local pose where `+X` is right, `+Y` is up, and `-Z` is forward."
        },
        {
          "name": "controller",
          "qualified_name": "Input.controller",
          "kind": "type",
          "declaration": "type controller = {\n  active: bool,\n  grip: Option.t<pose>,\n  aim: Option.t<pose>,\n  trigger: float,\n  squeeze: float,\n  thumbstick: point2,\n  primaryPressed: bool,\n  secondaryPressed: bool,\n  thumbstickPressed: bool,\n  menuPressed: bool\n}",
          "docs": "One XR controller's availability, poses, analog controls, and buttons."
        },
        {
          "name": "xr",
          "qualified_name": "Input.xr",
          "kind": "type",
          "declaration": "type xr = {\n  head: Option.t<pose>,\n  left: controller,\n  right: controller\n}",
          "docs": "Head and left/right controller state sampled from an XR runtime."
        },
        {
          "name": "gamepad",
          "qualified_name": "Input.gamepad",
          "kind": "type",
          "declaration": "type gamepad = {\n  leftStick: point2,\n  rightStick: point2,\n  leftTrigger: float,\n  rightTrigger: float,\n  south: bool,\n  east: bool,\n  west: bool,\n  north: bool,\n  leftBumper: bool,\n  rightBumper: bool,\n  leftStickPressed: bool,\n  rightStickPressed: bool,\n  dpadUp: bool,\n  dpadDown: bool,\n  dpadLeft: bool,\n  dpadRight: bool,\n  start: bool,\n  select: bool\n}",
          "docs": "The primary connected gamepad's held state, aligned to the standard\nmapping desktop and web pads share. Face buttons are POSITIONAL — `south`\nis the bottom face button (A on Xbox, Cross on PlayStation, B on\nNintendo) — because letter names swap between vendors. Sticks are `-1..1`\nwith up-positive `y` (the XR thumbstick convention); triggers are `0..1`.\nValues are raw — apply your own deadzone. Levels only: detect edges\nagainst your model, as XR games do. Native's windowed runtime and the\nweb runtime both poll the first connected standard-mapping pad each\nframe (on native, debug injection wins over the poll); while the\nwindow/document is unfocused or the clock is pinned a connected pad\nreads rest-level controls rather than `Option.None`. Browsers hide pads\nuntil a button is first pressed, and headless polls nothing — there this\nis `Option.None` unless injected."
        },
        {
          "name": "touchPoint",
          "qualified_name": "Input.touchPoint",
          "kind": "type",
          "declaration": "type touchPoint = { id: float, x: float, y: float }",
          "docs": "One touch contact in the same top-left-origin logical coordinate space as\n`mouse` (window points natively, CSS pixels on web). `id` is a small\nordinal stable for the contact's lifetime."
        },
        {
          "name": "touch",
          "qualified_name": "Input.touch",
          "kind": "type",
          "declaration": "type touch = {\n  touches: List<touchPoint>,\n  pressed: List<touchPoint>,\n  released: List<touchPoint>\n}",
          "docs": "Active touch contacts plus this step's transitions — the keyboard\ncontract for fingers: `touches` are held levels at current positions,\n`pressed`/`released` are de-duplicated one-step edges (a quick tap can\nappear in both while `touches` no longer carries it). A contact the\nplatform steals (gesture navigation) reports through `released`, never a\nsilently vanished touch. On the snapshot, `Option.Some` signals a touch\nsurface EXISTS (empty lists while idle — the cue to show touch UI);\n`Option.None` means no touch input at all."
        },
        {
          "name": "mouseButtons",
          "qualified_name": "Input.mouseButtons",
          "kind": "type",
          "declaration": "type mouseButtons = { left: bool, right: bool, middle: bool }",
          "docs": "A fixed set of mouse buttons. `mouse.buttons` uses it for held levels;\n`mouse.pressed` / `mouse.released` use it for one-step transitions."
        },
        {
          "name": "mouse",
          "qualified_name": "Input.mouse",
          "kind": "type",
          "declaration": "type mouse = {\n  x: float,\n  y: float,\n  surfaceWidth: float,\n  surfaceHeight: float,\n  buttons: mouseButtons,\n  pressed: mouseButtons,\n  released: mouseButtons\n}",
          "docs": "Mouse position in top-left-origin logical surface coordinates, the matching\nlogical extent, plus held levels and transitions since the previous fixed\nsimulation step. Desktop uses window points; web uses CSS pixels, so this\nstays stable across Retina/device-pixel-ratio changes. A quick click may be\nboth `pressed.left` and `released.left` in one sample."
        },
        {
          "name": "snapshot",
          "qualified_name": "Input.snapshot",
          "kind": "type",
          "declaration": "type snapshot = {\n  heldKeys: List<Key.t>,\n  pressedKeys: List<Key.t>,\n  releasedKeys: List<Key.t>,\n  mouse: mouse,\n  xr: Option.t<xr>,\n  gamepad: Option.t<gamepad>,\n  touch: Option.t<touch>\n}",
          "docs": "Keyboard/mouse levels and transitions for one fixed simulation step.\n\n`pressedKeys` and `releasedKeys` are de-duplicated transition sets. They\nsurvive render frames with no simulation step, are consumed by the first\ncatch-up step, and are empty on later steps. Native OS-repeat events still\nreach the legacy `input` hook, but do not repeat `pressedKeys`."
        }
      ]
    },
    {
      "name": "Effect",
      "group": "engine",
      "category": "Effects & messaging",
      "docs": "Commands returned beside a model and performed by the runtime.\n\nResults are converted into game messages and folded back through `update`.\nEffects remain outside the game's pure functional core.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Effect.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque effect command."
        },
        {
          "name": "none",
          "qualified_name": "Effect.none",
          "kind": "value",
          "declaration": "let none : () => t",
          "docs": "Produce no command."
        },
        {
          "name": "now",
          "qualified_name": "Effect.now",
          "kind": "value",
          "declaration": "let now : ((float) => 'msg) => t",
          "docs": "Read the current Unix time in seconds and tag the result as a message."
        },
        {
          "name": "random",
          "qualified_name": "Effect.random",
          "kind": "value",
          "declaration": "let random : ((float) => 'msg) => t",
          "docs": "Generate a random float in `0..1` and tag it as a message."
        },
        {
          "name": "batch",
          "qualified_name": "Effect.batch",
          "kind": "value",
          "declaration": "let batch : (List<t>) => t",
          "docs": "Combine effects to be performed together."
        },
        {
          "name": "send",
          "qualified_name": "Effect.send",
          "kind": "value",
          "declaration": "let send : (float, string) => t",
          "docs": "Send text over a live connection by connection ID."
        },
        {
          "name": "sendMsg",
          "qualified_name": "Effect.sendMsg",
          "kind": "value",
          "declaration": "let sendMsg : (float, 'a) => t",
          "docs": "Send a plain-data value over a live connection.\n\nThe peer receives `Net.Data(id, value)`. Functions and opaque host values\ncannot be sent."
        },
        {
          "name": "httpGet",
          "qualified_name": "Effect.httpGet",
          "kind": "value",
          "declaration": "let httpGet : (string, (Net.HttpResponse) => 'msg) => t",
          "docs": "Perform an HTTP GET and tag its `Net.HttpResponse` as a message."
        },
        {
          "name": "httpPost",
          "qualified_name": "Effect.httpPost",
          "kind": "value",
          "declaration": "let httpPost : (string, string, (Net.HttpResponse) => 'msg) => t",
          "docs": "Perform an HTTP POST with a text body and tag its response as a message."
        },
        {
          "name": "play",
          "qualified_name": "Effect.play",
          "kind": "value",
          "declaration": "let play : (Asset.Sound) => t",
          "docs": "Play a non-spatial sound once."
        },
        {
          "name": "playAt",
          "qualified_name": "Effect.playAt",
          "kind": "value",
          "declaration": "let playAt : (Asset.Sound, Vec3.t) => t",
          "docs": "Play a spatial sound once at a world position."
        },
        {
          "name": "playThen",
          "qualified_name": "Effect.playThen",
          "kind": "value",
          "declaration": "let playThen : (Asset.Sound, 'msg) => t",
          "docs": "Play a sound once and deliver a message when playback finishes.\n\nThe completion message is native-only; on wasm the sound plays but the\nmessage is not delivered, so do not gate game progress on it."
        },
        {
          "name": "preload",
          "qualified_name": "Effect.preload",
          "kind": "value",
          "declaration": "let preload : ('asset) => t",
          "docs": "Begin loading a model or texture before it is referenced by `draw`.\n\nThe imperative prefetch; the declarative default is simply that `draw`\nreferences the asset. The parameter is generic, but only an `Asset.Model`\nor `Asset.Texture` is accepted: an `Asset.Sound` is a teaching error (a\nsound decodes at play time and has no pending state), and a bare path\nstring is rejected toward `Asset.model` / `Asset.texture` like any other\nasset consumer. `preloadThen` takes the same values."
        },
        {
          "name": "preloadThen",
          "qualified_name": "Effect.preloadThen",
          "kind": "value",
          "declaration": "let preloadThen : ('asset, 'msg) => t",
          "docs": "Preload an asset and deliver a message when the load settles.\n\nSettlement includes success and failure; `Sub.assets` reports which assets\nfailed. Preloads count toward `Sub.assets` totals, and unlike `playThen`'s\ncompletion message this one is delivered on wasm too."
        }
      ]
    },
    {
      "name": "Sub",
      "group": "engine",
      "category": "Effects & messaging",
      "docs": "Declarative event sources returned by the optional `subscriptions` hook.\n\nEvents become game messages and are folded through `update` before `tick`.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Sub.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque subscription description."
        },
        {
          "name": "none",
          "qualified_name": "Sub.none",
          "kind": "value",
          "declaration": "let none : () => t",
          "docs": "Subscribe to no events."
        },
        {
          "name": "every",
          "qualified_name": "Sub.every",
          "kind": "value",
          "declaration": "let every : (Time.t, 'msg) => t",
          "docs": "Deliver a message at the requested interval.\n\nThe timer is STATELESS: it fires when an integer multiple of its period\nlies in the interval this frame covers, measured on the global time grid.\nSo a long frame that spans several boundaries fires ONCE (missed\nboundaries collapse rather than queueing up), and timers keep their\nphase across a hot reload."
        },
        {
          "name": "batch",
          "qualified_name": "Sub.batch",
          "kind": "value",
          "declaration": "let batch : (List<t>) => t",
          "docs": "Combine subscriptions."
        },
        {
          "name": "connect",
          "qualified_name": "Sub.connect",
          "kind": "value",
          "declaration": "let connect : (string, (Net.NetEvent) => 'msg) => t",
          "docs": "Maintain a client connection and tag its ordered `Net.NetEvent` values as messages.\n\nA failed attempt delivers `Net.Error`, then retries forever on the game-time\nclock after 250ms, doubling through 500ms, 1s, 2s, 4s, and at most 8s.\nEvery failed attempt remains observable as an `Error`; `Connected` resets\nthe next retry to 250ms. Dropping the subscription cancels the connection\nand its pending retry."
        },
        {
          "name": "listen",
          "qualified_name": "Sub.listen",
          "kind": "value",
          "declaration": "let listen : (string, (Net.NetEvent) => 'msg) => t",
          "docs": "Maintain a server listener and tag its `Net.NetEvent` values as messages."
        },
        {
          "name": "AssetFailure",
          "qualified_name": "Sub.AssetFailure",
          "kind": "type",
          "declaration": "type AssetFailure = { path: string, error: string }",
          "docs": "One asset byte-load failure.\n\nOnly a failed BYTE load lands here — an asset whose bytes arrived but did\nnot decode counts as loaded (it renders its fallback)."
        },
        {
          "name": "AssetProgress",
          "qualified_name": "Sub.AssetProgress",
          "kind": "type",
          "declaration": "type AssetProgress = { loaded: float, total: float, failed: List<AssetFailure> }",
          "docs": "A snapshot of loaded, total, and failed assets.\n\nAll assets are settled when `total > 0` and\n`loaded + List.length(failed) == total`. Failures never join `loaded`, and\nframe one can legitimately deliver `0 / 0`, so a loading screen must gate on\nthe whole expression rather than on `loaded == total`."
        },
        {
          "name": "assets",
          "qualified_name": "Sub.assets",
          "kind": "value",
          "declaration": "let assets : ((AssetProgress) => 'msg) => t",
          "docs": "Subscribe to asset-loading snapshots, including the initial state.\n\nDelivery is driven by CHANGE, not by the time grid: the tagger fires\nwhenever the shell's snapshot differs from the last one it delivered\n(including the first one on frame one), so this is not a per-frame poll.\nLike every subscription it requires an `update` hook."
        }
      ]
    },
    {
      "name": "Persistence",
      "group": "engine",
      "category": "Effects & messaging",
      "docs": "Durable local state — the save slot that outlives the process.\n\nThe model is the game's live state and hot-reload preserves it, but\nquitting does not: a slot is where progress goes to survive. Both\nfunctions produce an ordinary `Effect.t`, performed by the same broker as\nevery other effect, so a save is deterministic under replay like anything\nelse.\n\nThis is the simple path for the common case — one autosave, saved whole and\nread back at boot. It deliberately says nothing about where the bytes live;\na general file API over a virtual user namespace is planned, and these two\nfunctions are meant to keep reading as the two-line convenience layer over\nit.\n\nSlots belong to the running project: natively `<project>/.functor/saves/`,\nin the browser a page- and entry-scoped `localStorage` key. One process\nruns one game, so the store root is process-global. The Quest/embedded\nproducer does not name a writable project root yet — there slots follow the\nprocess working directory.",
      "items": [
        {
          "name": "save",
          "qualified_name": "Persistence.save",
          "kind": "value",
          "declaration": "let save : (string, 'a) => Effect.t",
          "docs": "Persist a plain-data value in a local save slot.\n\nThe durable dual of the model: a slot survives quitting the game, where\nhot-reload's preserved model does not. The value is carried by the same\ncodec as `Effect.sendMsg` — numbers, strings, bools, lists, maps, tuples,\nrecords and variants of those — so functions and opaque host values are a\nteaching error at the call site. Saving the whole model is the common\ncase; save what `init` can rebuild from.\n\nThe slot is a NAME, not a path: letters, digits, `_` and `-` (1–64\ncharacters). Natively it lands in `<project>/.functor/saves/<slot>.json`,\nwritten atomically; in the browser it is one project-scoped\n`localStorage` key. Writing the same slot again replaces it."
        },
        {
          "name": "load",
          "qualified_name": "Persistence.load",
          "kind": "value",
          "declaration": "let load : (string, (Option.t<'a>) => 'msg) => Effect.t",
          "docs": "Read a local save slot back and tag it as a message.\n\nThe tagger receives `Option.Some(value)` — the value exactly as saved —\nor `Option.None` when the slot has never been written or what is there\ncannot be read at all (a corrupt or hand-mangled file): an unreadable\nsave degrades to \"no save\", it never stops the game.\n\nA save that reads back fine but was written by an OLDER MODEL SHAPE is\nstill `Option.Some` — nothing checks the shape for you. Handle that the\nway you would any external data: change the slot name when the model\nchanges incompatibly, or save a record carrying your own version field\nand match on it.\n\nUsually issued once at boot from the first `tick`."
        }
      ]
    },
    {
      "name": "AudioScene",
      "group": "engine",
      "category": "Audio",
      "docs": "Declarative audio scenes returned by the optional `soundScape` hook.",
      "items": [
        {
          "name": "t",
          "qualified_name": "AudioScene.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque audio scene."
        },
        {
          "name": "create",
          "qualified_name": "AudioScene.create",
          "kind": "value",
          "declaration": "let create : (List<AudioSource.t>) => t",
          "docs": "Create an audio scene from continuous sources."
        },
        {
          "name": "empty",
          "qualified_name": "AudioScene.empty",
          "kind": "value",
          "declaration": "let empty : () => t",
          "docs": "Create an audio scene with no sources."
        }
      ]
    },
    {
      "name": "AudioSource",
      "group": "engine",
      "category": "Audio",
      "docs": "Continuous, declarative voices used in an `AudioScene`.\n\nSources are keyed for cross-frame identity so the runtime can reconcile a\nlive voice instead of restarting it every frame.",
      "items": [
        {
          "name": "t",
          "qualified_name": "AudioSource.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque continuous audio source."
        },
        {
          "name": "ambient",
          "qualified_name": "AudioSource.ambient",
          "kind": "value",
          "declaration": "let ambient : (string, Asset.Sound) => t",
          "docs": "Create a non-spatial source identified by a stable key."
        },
        {
          "name": "at",
          "qualified_name": "AudioSource.at",
          "kind": "value",
          "declaration": "let at : (string, Asset.Sound, Vec3.t) => t",
          "docs": "Create a spatial source at a world position, identified by a stable key."
        },
        {
          "name": "gain",
          "qualified_name": "AudioSource.gain",
          "kind": "value",
          "declaration": "let gain : (float, t) => t",
          "docs": "Set LINEAR source gain, where `1.0` is full volume; the source is last for\npiping."
        }
      ]
    },
    {
      "name": "Ui",
      "group": "engine",
      "category": "UI",
      "docs": "Lightweight game UI views returned by the optional `ui` hook.\n\nCompose text and widgets into rows or columns, then pin them to a screen\nanchor with `Ui.panel`. Text is 14pt monospace.\n\nLike `draw`, the hook is a pure function of the model, and the widgets are\nCONTROLLED: a view shows what the model says, and interacting with it sends\na message — the model stays the only state. Those messages (a button's, and\na slider's or text input's tagger result) are folded through `update`, so a\nprogram with interactive UI must define that hook.",
      "items": [
        {
          "name": "view",
          "qualified_name": "Ui.view",
          "kind": "type",
          "declaration": "type view = host",
          "docs": "An opaque UI view."
        },
        {
          "name": "anchor",
          "qualified_name": "Ui.anchor",
          "kind": "type",
          "declaration": "type anchor = host",
          "docs": "An opaque screen anchor."
        },
        {
          "name": "text",
          "qualified_name": "Ui.text",
          "kind": "value",
          "declaration": "let text : (string) => view",
          "docs": "Create a text view."
        },
        {
          "name": "textColor",
          "qualified_name": "Ui.textColor",
          "kind": "value",
          "declaration": "let textColor : (Color.t, string) => view",
          "docs": "Create a colored text view."
        },
        {
          "name": "column",
          "qualified_name": "Ui.column",
          "kind": "value",
          "declaration": "let column : (List<view>) => view",
          "docs": "Stack views vertically."
        },
        {
          "name": "row",
          "qualified_name": "Ui.row",
          "kind": "value",
          "declaration": "let row : (List<view>) => view",
          "docs": "Arrange views horizontally."
        },
        {
          "name": "panel",
          "qualified_name": "Ui.panel",
          "kind": "value",
          "declaration": "let panel : (anchor, view) => view",
          "docs": "Pin a view to an anchor; the view is last for piping."
        },
        {
          "name": "topLeft",
          "qualified_name": "Ui.topLeft",
          "kind": "value",
          "declaration": "let topLeft : () => anchor",
          "docs": "Anchor a panel to the top-left corner."
        },
        {
          "name": "topRight",
          "qualified_name": "Ui.topRight",
          "kind": "value",
          "declaration": "let topRight : () => anchor",
          "docs": "Anchor a panel to the top-right corner."
        },
        {
          "name": "bottomLeft",
          "qualified_name": "Ui.bottomLeft",
          "kind": "value",
          "declaration": "let bottomLeft : () => anchor",
          "docs": "Anchor a panel to the bottom-left corner."
        },
        {
          "name": "bottomRight",
          "qualified_name": "Ui.bottomRight",
          "kind": "value",
          "declaration": "let bottomRight : () => anchor",
          "docs": "Anchor a panel to the bottom-right corner."
        },
        {
          "name": "center",
          "qualified_name": "Ui.center",
          "kind": "value",
          "declaration": "let center : () => anchor",
          "docs": "Anchor a panel to the center of the screen."
        },
        {
          "name": "button",
          "qualified_name": "Ui.button",
          "kind": "value",
          "declaration": "let button : (string, 'msg) => view",
          "docs": "Create a button that delivers a message through `update` when clicked.\n\nThe message is delivered VERBATIM, like `Sub.every`'s — so the link between\nits type and `update`'s is a runtime check, not a static one."
        },
        {
          "name": "slider",
          "qualified_name": "Ui.slider",
          "kind": "value",
          "declaration": "let slider : (float, float, float, 'tagger) => view",
          "docs": "Create a controlled slider from minimum, maximum, value, and tagger.\n\nA drag applies the tagger to the new value and folds the resulting message\nthrough `update`. The maximum must exceed the minimum, and the tagger must\nbe a function or constructor."
        },
        {
          "name": "textInput",
          "qualified_name": "Ui.textInput",
          "kind": "value",
          "declaration": "let textInput : (string, 'tagger) => view",
          "docs": "Create a controlled text input whose tagger receives edited text.\n\nWhile a field is FOCUSED the game's `input` hook is suppressed, so keys\ntype into the field instead of driving the game; Escape defocuses the field\nfirst and releases the cursor second. An `update` that transforms the text\nresets the caret to the end."
        }
      ]
    },
    {
      "name": "Html",
      "group": "engine",
      "category": "UI",
      "docs": "Elm-style HTML trees returned by the optional `webview` hook.\n\nNative renders the tree over the 3D frame; wasm renders it as a DOM overlay\nabove the canvas.",
      "items": [
        {
          "name": "node",
          "qualified_name": "Html.node",
          "kind": "type",
          "declaration": "type node = host",
          "docs": "An opaque HTML node."
        },
        {
          "name": "text",
          "qualified_name": "Html.text",
          "kind": "value",
          "declaration": "let text : (string) => node",
          "docs": "Create an escaped text node."
        },
        {
          "name": "element",
          "qualified_name": "Html.element",
          "kind": "value",
          "declaration": "let element : (string, List<Attr.t>, List<node>) => node",
          "docs": "Create an element from a safe tag name, attributes, and children.\n\nNames must start with a letter and then contain only letters, digits, or\ndashes. Script-capable tags such as `script` and `iframe` are refused."
        },
        {
          "name": "div",
          "qualified_name": "Html.div",
          "kind": "value",
          "declaration": "let div : (List<Attr.t>, List<node>) => node",
          "docs": "Create a `div` element."
        },
        {
          "name": "span",
          "qualified_name": "Html.span",
          "kind": "value",
          "declaration": "let span : (List<Attr.t>, List<node>) => node",
          "docs": "Create a `span` element."
        },
        {
          "name": "button",
          "qualified_name": "Html.button",
          "kind": "value",
          "declaration": "let button : (List<Attr.t>, List<node>) => node",
          "docs": "Create a `button` element."
        },
        {
          "name": "h1",
          "qualified_name": "Html.h1",
          "kind": "value",
          "declaration": "let h1 : (List<Attr.t>, List<node>) => node",
          "docs": "Create an `h1` heading element."
        },
        {
          "name": "h2",
          "qualified_name": "Html.h2",
          "kind": "value",
          "declaration": "let h2 : (List<Attr.t>, List<node>) => node",
          "docs": "Create an `h2` heading element."
        },
        {
          "name": "p",
          "qualified_name": "Html.p",
          "kind": "value",
          "declaration": "let p : (List<Attr.t>, List<node>) => node",
          "docs": "Create a paragraph element."
        },
        {
          "name": "input",
          "qualified_name": "Html.input",
          "kind": "value",
          "declaration": "let input : (List<Attr.t>) => node",
          "docs": "Create a controlled, single-line text input.\n\nPair it with `Attr.value` and `Attr.onInput`."
        },
        {
          "name": "style",
          "qualified_name": "Html.style",
          "kind": "value",
          "declaration": "let style : (string) => node",
          "docs": "Create a `style` element containing a raw CSS stylesheet."
        }
      ]
    },
    {
      "name": "Attr",
      "group": "engine",
      "category": "UI",
      "docs": "Attributes and event handlers for `Html` elements.\n\nEvent attributes deliver messages through the game's `update` function.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Attr.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque HTML attribute."
        },
        {
          "name": "class",
          "qualified_name": "Attr.class",
          "kind": "value",
          "declaration": "let class : (string) => t",
          "docs": "Set the element's CSS class string."
        },
        {
          "name": "style",
          "qualified_name": "Attr.style",
          "kind": "value",
          "declaration": "let style : (string) => t",
          "docs": "Set the element's raw inline `style` string."
        },
        {
          "name": "styles",
          "qualified_name": "Attr.styles",
          "kind": "value",
          "declaration": "let styles : (List<Style.t>) => t",
          "docs": "Combine typed `Style` values into one inline style attribute."
        },
        {
          "name": "id",
          "qualified_name": "Attr.id",
          "kind": "value",
          "declaration": "let id : (string) => t",
          "docs": "Set the element's `id`."
        },
        {
          "name": "attr",
          "qualified_name": "Attr.attr",
          "kind": "value",
          "declaration": "let attr : (string, string) => t",
          "docs": "Set a safe attribute by name and value.\n\nNames must start with a letter and then contain only letters, digits, or\ndashes. Executable, navigating, document-embedding, and runtime-reserved\nnames (`on*`, `href`, `action`, `formaction`, `srcdoc`, and `data-fn-*`) are\nrefused."
        },
        {
          "name": "value",
          "qualified_name": "Attr.value",
          "kind": "value",
          "declaration": "let value : (string) => t",
          "docs": "Set the controlled value of an `Html.input`."
        },
        {
          "name": "placeholder",
          "qualified_name": "Attr.placeholder",
          "kind": "value",
          "declaration": "let placeholder : (string) => t",
          "docs": "Set an input's placeholder text."
        },
        {
          "name": "onClick",
          "qualified_name": "Attr.onClick",
          "kind": "value",
          "declaration": "let onClick : ('msg) => t",
          "docs": "Deliver a message through `update` when the element is clicked.\n\nA click on a descendant counts too, through ordinary DOM bubbling. The\nmessage is delivered verbatim, like `Ui.button`'s."
        },
        {
          "name": "onInput",
          "qualified_name": "Attr.onInput",
          "kind": "value",
          "declaration": "let onInput : ('tagger) => t",
          "docs": "Apply a tagger to edited text and deliver its message through `update`.\n\nWorks on both shells. Clicking the input focuses it, after which keys type\ninto the field rather than reaching the game's `input` hook (Escape\ndefocuses first, releases the cursor second), and focus survives the\nper-edit re-render. As with `Ui.textInput`, an `update` that transforms the\ntext resets the caret to the end. IME composition (CJK, dead keys) is\ndeferred on native."
        }
      ]
    },
    {
      "name": "Style",
      "group": "engine",
      "category": "UI",
      "docs": "Typed inline CSS declarations combined with `Attr.styles`.\n\nConstructors format their values immediately and share `Color.t` with the\n3D APIs. Use `Html.style` for selectors, pseudo-classes, and keyframes.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Style.t",
          "kind": "type",
          "declaration": "type t = host",
          "docs": "An opaque inline CSS declaration."
        },
        {
          "name": "flexRow",
          "qualified_name": "Style.flexRow",
          "kind": "value",
          "declaration": "let flexRow : () => t",
          "docs": "Use horizontal flexbox layout."
        },
        {
          "name": "flexColumn",
          "qualified_name": "Style.flexColumn",
          "kind": "value",
          "declaration": "let flexColumn : () => t",
          "docs": "Use vertical flexbox layout."
        },
        {
          "name": "gapPx",
          "qualified_name": "Style.gapPx",
          "kind": "value",
          "declaration": "let gapPx : (float) => t",
          "docs": "Set the flex/grid gap in pixels."
        },
        {
          "name": "justifyStart",
          "qualified_name": "Style.justifyStart",
          "kind": "value",
          "declaration": "let justifyStart : () => t",
          "docs": "Align content to the start of the main axis."
        },
        {
          "name": "justifyCenter",
          "qualified_name": "Style.justifyCenter",
          "kind": "value",
          "declaration": "let justifyCenter : () => t",
          "docs": "Center content on the main axis."
        },
        {
          "name": "justifyEnd",
          "qualified_name": "Style.justifyEnd",
          "kind": "value",
          "declaration": "let justifyEnd : () => t",
          "docs": "Align content to the end of the main axis."
        },
        {
          "name": "justifyBetween",
          "qualified_name": "Style.justifyBetween",
          "kind": "value",
          "declaration": "let justifyBetween : () => t",
          "docs": "Distribute content with space between items."
        },
        {
          "name": "alignStart",
          "qualified_name": "Style.alignStart",
          "kind": "value",
          "declaration": "let alignStart : () => t",
          "docs": "Align items to the start of the cross axis."
        },
        {
          "name": "alignCenter",
          "qualified_name": "Style.alignCenter",
          "kind": "value",
          "declaration": "let alignCenter : () => t",
          "docs": "Center items on the cross axis."
        },
        {
          "name": "alignEnd",
          "qualified_name": "Style.alignEnd",
          "kind": "value",
          "declaration": "let alignEnd : () => t",
          "docs": "Align items to the end of the cross axis."
        },
        {
          "name": "widthPx",
          "qualified_name": "Style.widthPx",
          "kind": "value",
          "declaration": "let widthPx : (float) => t",
          "docs": "Set width in pixels."
        },
        {
          "name": "widthPct",
          "qualified_name": "Style.widthPct",
          "kind": "value",
          "declaration": "let widthPct : (float) => t",
          "docs": "Set width as a percentage of the parent."
        },
        {
          "name": "heightPx",
          "qualified_name": "Style.heightPx",
          "kind": "value",
          "declaration": "let heightPx : (float) => t",
          "docs": "Set height in pixels."
        },
        {
          "name": "heightPct",
          "qualified_name": "Style.heightPct",
          "kind": "value",
          "declaration": "let heightPct : (float) => t",
          "docs": "Set height as a percentage of the parent."
        },
        {
          "name": "paddingPx",
          "qualified_name": "Style.paddingPx",
          "kind": "value",
          "declaration": "let paddingPx : (float) => t",
          "docs": "Set padding on all sides in pixels."
        },
        {
          "name": "marginPx",
          "qualified_name": "Style.marginPx",
          "kind": "value",
          "declaration": "let marginPx : (float) => t",
          "docs": "Set margin on all sides in pixels."
        },
        {
          "name": "color",
          "qualified_name": "Style.color",
          "kind": "value",
          "declaration": "let color : (Color.t) => t",
          "docs": "Set the text color."
        },
        {
          "name": "background",
          "qualified_name": "Style.background",
          "kind": "value",
          "declaration": "let background : (Color.t) => t",
          "docs": "Set the background color."
        },
        {
          "name": "fontSizePx",
          "qualified_name": "Style.fontSizePx",
          "kind": "value",
          "declaration": "let fontSizePx : (float) => t",
          "docs": "Set font size in pixels."
        },
        {
          "name": "bold",
          "qualified_name": "Style.bold",
          "kind": "value",
          "declaration": "let bold : () => t",
          "docs": "Use a bold font weight."
        },
        {
          "name": "textCenter",
          "qualified_name": "Style.textCenter",
          "kind": "value",
          "declaration": "let textCenter : () => t",
          "docs": "Center-align text."
        },
        {
          "name": "borderPx",
          "qualified_name": "Style.borderPx",
          "kind": "value",
          "declaration": "let borderPx : (float, Color.t) => t",
          "docs": "Add a solid border with pixel width and color."
        },
        {
          "name": "roundedPx",
          "qualified_name": "Style.roundedPx",
          "kind": "value",
          "declaration": "let roundedPx : (float) => t",
          "docs": "Set border radius in pixels."
        },
        {
          "name": "opacity",
          "qualified_name": "Style.opacity",
          "kind": "value",
          "declaration": "let opacity : (float) => t",
          "docs": "Set opacity in `0..1`. A value outside that range is an error, not a clamp."
        },
        {
          "name": "raw",
          "qualified_name": "Style.raw",
          "kind": "value",
          "declaration": "let raw : (string, string) => t",
          "docs": "Create an inline declaration for an arbitrary CSS property and value.\n\nProperty names must start with a letter and then contain only letters,\ndigits, or dashes."
        }
      ]
    },
    {
      "name": "Asset",
      "group": "engine",
      "category": "Assets",
      "docs": "Typed locators for models, textures, and sounds.\n\nEach asset kind has its own branded value, making wrong-kind uses a\ntype error. Prefer constants generated by `functor import`; constructors\nremain available where a locator enters the program dynamically.\n\nAsset consumers — `Scene.model`, `Sprite.image`/`imageRegion`,\n`Terrain.heightmap`/`textured`, `Effect.play`/`playAt`/`playThen`/\n`preload`/`preloadThen`, `AudioSource.ambient`/`at` — take these branded\nvalues ONLY: a\nbare path string there is a check error and, at runtime, a teaching error\npointing at the generated manifest, and an asset of the wrong kind names\nthe constructor that was wanted. (`Texture.file` paths, `Skybox.files`\nfaces, `Anim.clip` names, and `AudioSource` keys are not asset locators and\nstay plain strings.)",
      "items": [
        {
          "name": "Model",
          "qualified_name": "Asset.Model",
          "kind": "type",
          "declaration": "type Model = host",
          "docs": "A model asset locator."
        },
        {
          "name": "Texture",
          "qualified_name": "Asset.Texture",
          "kind": "type",
          "declaration": "type Texture = host",
          "docs": "A texture asset locator."
        },
        {
          "name": "Sound",
          "qualified_name": "Asset.Sound",
          "kind": "type",
          "declaration": "type Sound = host",
          "docs": "A sound asset locator."
        },
        {
          "name": "model",
          "qualified_name": "Asset.model",
          "kind": "value",
          "declaration": "let model : (string) => Model",
          "docs": "Construct a model locator from a relative path or URL."
        },
        {
          "name": "texture",
          "qualified_name": "Asset.texture",
          "kind": "value",
          "declaration": "let texture : (string) => Texture",
          "docs": "Construct a texture locator from a relative path or URL."
        },
        {
          "name": "sound",
          "qualified_name": "Asset.sound",
          "kind": "value",
          "declaration": "let sound : (string) => Sound",
          "docs": "Construct a sound locator from a relative path or URL."
        },
        {
          "name": "whilePending",
          "qualified_name": "Asset.whilePending",
          "kind": "value",
          "declaration": "let whilePending : ('placeholder, 'asset) => 'asset",
          "docs": "Use another asset of the same kind while a model or texture is loading.\n\nModels and textures only — a sound decodes at play time and has no pending\nstate, so asking for one is a teaching error. The placeholder is just\nanother asset of the same kind, so placeholders chain. The requested asset\nis last for piping. Failed loads use the normal fallback because failure is\nno longer pending, and `Sub.assets` still reports the failure."
        }
      ]
    },
    {
      "name": "List",
      "group": "stdlib",
      "category": "Collections",
      "docs": "Immutable list operations.\n\nFunctor Lang has no loops: iteration is `List.map`, `List.filter`, and\n`List.fold`, which run iteratively in the interpreter and so consume no\nevaluation depth (unlike a hand-rolled recursive walk, which trips the\nrecursion cap).\n\nEvery function takes its list LAST, so it threads through the thread-last\npipeline operator: `xs |> List.map(f)` is exactly `List.map(f, xs)`.\n\nThe partial accessors — `nth`, `head`, `last`, `find` — answer with\n`Option.t`, never a sentinel, so the absent case has to be handled.",
      "items": [
        {
          "name": "map",
          "qualified_name": "List.map",
          "kind": "value",
          "declaration": "let map : (('a) => 'b, List<'a>) => List<'b>",
          "docs": "Apply `fn` to every element, preserving order and length."
        },
        {
          "name": "indexedMap",
          "qualified_name": "List.indexedMap",
          "kind": "value",
          "declaration": "let indexedMap : ((float, 'a) => 'b, List<'a>) => List<'b>",
          "docs": "Apply `fn` to every element with its 0-based index, as `fn(index, element)`\n— index FIRST, unlike the subject-last list argument."
        },
        {
          "name": "filter",
          "qualified_name": "List.filter",
          "kind": "value",
          "declaration": "let filter : (('a) => bool, List<'a>) => List<'a>",
          "docs": "Keep the elements the predicate accepts, in order."
        },
        {
          "name": "fold",
          "qualified_name": "List.fold",
          "kind": "value",
          "declaration": "let fold : (('b, 'a) => 'b, 'b, List<'a>) => 'b",
          "docs": "Reduce left-to-right from an initial accumulator, calling `fn(acc, element)`.\nThis is the iteration primitive to reach for when `map` and `filter` do not\nfit — a recursive walk hits the interpreter's recursion cap around 40–60\nelements, while `fold` has no such limit."
        },
        {
          "name": "concatMap",
          "qualified_name": "List.concatMap",
          "kind": "value",
          "declaration": "let concatMap : (('a) => List<'b>, List<'a>) => List<'b>",
          "docs": "Map each element to a list and concatenate the results (one level)."
        },
        {
          "name": "range",
          "qualified_name": "List.range",
          "kind": "value",
          "declaration": "let range : (float) => List<float>",
          "docs": "`[0, 1, … n - 1]`. A non-positive `n` gives the empty list; a count that\nis not finite, or above one million, is an error. A fractional count\ntruncates toward zero, so `List.range(3.7)` is `[0, 1, 2]`."
        },
        {
          "name": "grid",
          "qualified_name": "List.grid",
          "kind": "value",
          "declaration": "let grid : ((float, float) => 'a, float, float) => List<List<'a>>",
          "docs": "Build a `rows` x `cols` grid by calling `fn(row, col)` for every cell, both\n0-based — the procedural-heightmap shape, e.g.\n`Scene.heightmap(List.grid(height, rows, cols))`. Both counts must be\nwhole and non-negative, and the cells must total at most one million."
        },
        {
          "name": "maximum",
          "qualified_name": "List.maximum",
          "kind": "value",
          "declaration": "let maximum : (List<float>) => Option.t<float>",
          "docs": "The largest number in the list as `Option.Some`, or `Option.None` for an\nempty list — partial like `nth`/`head`/`last`/`find`. NaN elements are\nignored unless every element is NaN."
        },
        {
          "name": "minimum",
          "qualified_name": "List.minimum",
          "kind": "value",
          "declaration": "let minimum : (List<float>) => Option.t<float>",
          "docs": "The smallest number in the list as `Option.Some`, or `Option.None` for an\nempty list — `maximum`'s mirror, with the same NaN rule."
        },
        {
          "name": "sum",
          "qualified_name": "List.sum",
          "kind": "value",
          "declaration": "let sum : (List<float>) => float",
          "docs": "The sum of the numbers. The empty list sums to `0.0`."
        },
        {
          "name": "length",
          "qualified_name": "List.length",
          "kind": "value",
          "declaration": "let length : (List<'a>) => float",
          "docs": "How many elements the list has, as a number."
        },
        {
          "name": "isEmpty",
          "qualified_name": "List.isEmpty",
          "kind": "value",
          "declaration": "let isEmpty : (List<'a>) => bool",
          "docs": "Whether the list has no elements."
        },
        {
          "name": "reverse",
          "qualified_name": "List.reverse",
          "kind": "value",
          "declaration": "let reverse : (List<'a>) => List<'a>",
          "docs": "The list in reverse order."
        },
        {
          "name": "append",
          "qualified_name": "List.append",
          "kind": "value",
          "declaration": "let append : (List<'a>, List<'a>) => List<'a>",
          "docs": "The piped list followed by `other`: `xs |> List.append(ys)` is `xs` then\n`ys`."
        },
        {
          "name": "flatten",
          "qualified_name": "List.flatten",
          "kind": "value",
          "declaration": "let flatten : (List<List<'a>>) => List<'a>",
          "docs": "Concatenate a list of lists, one level deep."
        },
        {
          "name": "zip",
          "qualified_name": "List.zip",
          "kind": "value",
          "declaration": "let zip : (List<'b>, List<'a>) => List<('a, 'b)>",
          "docs": "Pair each element of the piped list with the element of `other` at the same\nindex — the PIPED list fills the first slot of every tuple. The result\ntruncates to the shorter of the two."
        },
        {
          "name": "sortBy",
          "qualified_name": "List.sortBy",
          "kind": "value",
          "declaration": "let sortBy : (('a) => float, List<'a>) => List<'a>",
          "docs": "Sort ascending by the number `fn` returns. The sort is STABLE and calls\n`fn` exactly once per element. NaN keys sort last, tied with each other\nregardless of sign, so the order is identical on every platform; `-0.0` and\n`0.0` tie."
        },
        {
          "name": "take",
          "qualified_name": "List.take",
          "kind": "value",
          "declaration": "let take : (float, List<'a>) => List<'a>",
          "docs": "The first `count` elements, saturating: past the end gives the whole list,\nand a negative count behaves as `0`. A fractional count truncates toward\nzero rather than raising — unlike `nth`, where a fractional index is a\ncaller bug."
        },
        {
          "name": "drop",
          "qualified_name": "List.drop",
          "kind": "value",
          "declaration": "let drop : (float, List<'a>) => List<'a>",
          "docs": "Everything after the first `count` elements, saturating: past the end gives\nthe empty list, and a negative count behaves as `0`. A fractional count\ntruncates, like `take`."
        },
        {
          "name": "any",
          "qualified_name": "List.any",
          "kind": "value",
          "declaration": "let any : (('a) => bool, List<'a>) => bool",
          "docs": "Whether ANY element satisfies the predicate, short-circuiting at the first\none that does. The empty list is `false`."
        },
        {
          "name": "all",
          "qualified_name": "List.all",
          "kind": "value",
          "declaration": "let all : (('a) => bool, List<'a>) => bool",
          "docs": "Whether EVERY element satisfies the predicate, short-circuiting at the\nfirst one that does not. The empty list is `true`."
        },
        {
          "name": "nth",
          "qualified_name": "List.nth",
          "kind": "value",
          "declaration": "let nth : (float, List<'a>) => Option.t<'a>",
          "docs": "The element at a 0-based `index`, or `Option.None` when the index is out of\nrange. An index that is not a whole, finite number is an ERROR rather than\nan absence — that is a caller bug, not a missing element."
        },
        {
          "name": "head",
          "qualified_name": "List.head",
          "kind": "value",
          "declaration": "let head : (List<'a>) => Option.t<'a>",
          "docs": "The first element, or `Option.None` for an empty list."
        },
        {
          "name": "last",
          "qualified_name": "List.last",
          "kind": "value",
          "declaration": "let last : (List<'a>) => Option.t<'a>",
          "docs": "The last element, or `Option.None` for an empty list."
        },
        {
          "name": "find",
          "qualified_name": "List.find",
          "kind": "value",
          "declaration": "let find : (('a) => bool, List<'a>) => Option.t<'a>",
          "docs": "The first element satisfying the predicate, or `Option.None` when none\ndoes. It stops at that first match rather than scanning the whole list."
        }
      ]
    },
    {
      "name": "Map",
      "group": "stdlib",
      "category": "Collections",
      "docs": "Immutable keyed collections.\n\nA `Map` is plain data: it compares structurally, displays, snapshots, and\nsurvives hot reload. Every operation returns a NEW map rather than mutating\nthe old one, and every function takes the map LAST so it threads through a\npipeline.\n\nKeys are bounded to `bool`, FINITE `float`, and `string`. Inference keeps a\nmap homogeneous in ordinary code, and a generic or `unknown` seam is\nchecked again at runtime; NaN and the infinities are refused, while `-0.0`\nand `0.0` are the same key.\n\nEvery map is stored in one canonical key order — bool before float before\nstring, then `false` before `true`, ascending numerically, and strings by\nUnicode scalar value (not locale-aware). So `values`, `toList`, structural\nequality, and display all agree byte-for-byte between native and wasm.\n\n`get` and `member` are logarithmic; the immutable `insert` and `remove`\ncopy the ordered storage and are linear, as are `values` and `toList`;\n`fromList` is O(n log n).",
      "items": [
        {
          "name": "empty",
          "qualified_name": "Map.empty",
          "kind": "value",
          "declaration": "let empty : () => Map<'a, 'b>",
          "docs": "The empty map."
        },
        {
          "name": "get",
          "qualified_name": "Map.get",
          "kind": "value",
          "declaration": "let get : ('a, Map<'a, 'b>) => Option.t<'b>",
          "docs": "The value stored under `key`, or `Option.None` when the map has no such\nkey."
        },
        {
          "name": "insert",
          "qualified_name": "Map.insert",
          "kind": "value",
          "declaration": "let insert : ('a, 'b, Map<'a, 'b>) => Map<'a, 'b>",
          "docs": "The map with `key` bound to `value`, replacing any existing binding."
        },
        {
          "name": "remove",
          "qualified_name": "Map.remove",
          "kind": "value",
          "declaration": "let remove : ('a, Map<'a, 'b>) => Map<'a, 'b>",
          "docs": "The map without `key`. Removing an absent key is not an error."
        },
        {
          "name": "member",
          "qualified_name": "Map.member",
          "kind": "value",
          "declaration": "let member : ('a, Map<'a, 'b>) => bool",
          "docs": "Whether the map holds a binding for `key`."
        },
        {
          "name": "values",
          "qualified_name": "Map.values",
          "kind": "value",
          "declaration": "let values : (Map<'a, 'b>) => List<'b>",
          "docs": "Every value, in canonical key order."
        },
        {
          "name": "toList",
          "qualified_name": "Map.toList",
          "kind": "value",
          "declaration": "let toList : (Map<'a, 'b>) => List<('a, 'b)>",
          "docs": "Every `(key, value)` pair, in canonical key order."
        },
        {
          "name": "fromList",
          "qualified_name": "Map.fromList",
          "kind": "value",
          "declaration": "let fromList : (List<('a, 'b)>) => Map<'a, 'b>",
          "docs": "Build a map from `(key, value)` pairs. For a repeated key the LAST pair\nwins."
        }
      ]
    },
    {
      "name": "Text",
      "group": "stdlib",
      "category": "Text",
      "docs": "String building, formatting, and inspection.\n\nThere is no string-concatenation operator and no character type:\ninterpolation (`$\"score: {n}\"`) covers most formatting, `Text.concat` joins\ntwo strings, and single characters are one-character STRINGS.\n\nFunctions with a clear subject take the string LAST so they thread through\na pipeline: `s |> Text.contains(\"ab\")` is `Text.contains(\"ab\", s)`.\n\nLengths and character splits count **Unicode scalar values** — not bytes,\nand not grapheme clusters. `Text.length(s)` always equals\n`List.length(Text.chars(s))`.",
      "items": [
        {
          "name": "concat",
          "qualified_name": "Text.concat",
          "kind": "value",
          "declaration": "let concat : (string, string) => string",
          "docs": "`a` followed by `b`.\n\nThe subject is LAST here too, which means piping PREPENDS: the piped\nstring lands in `b`, so `\"SUF\" |> Text.concat(\"PRE\")` is `\"PRESUF\"`."
        },
        {
          "name": "fromFloat",
          "qualified_name": "Text.fromFloat",
          "kind": "value",
          "declaration": "let fromFloat : (float) => string",
          "docs": "A number rendered in Functor Lang's canonical display form — the same text\nstring interpolation produces. That is the shortest round-tripping form, so\n`1.0` renders as `\"1\"`; use `Text.fixed` when a HUD needs a stable width."
        },
        {
          "name": "fixed",
          "qualified_name": "Text.fixed",
          "kind": "value",
          "declaration": "let fixed : (float, float) => string",
          "docs": "A number rendered with exactly `decimals` digits after the point.\n`Text.fixed(42.0, 0.0)` is `\"42\"`, the integer-formatting shape — the one\nto reach for in a HUD. The digit count must be a whole number from 0 to\n12; anything else is an error. Unlike the string functions, the NUMBER\ncomes first, so this cannot be piped on: write `Text.fixed(hp, 0.0)`, not\n`hp |> Text.fixed(0.0)`."
        },
        {
          "name": "toBullets",
          "qualified_name": "Text.toBullets",
          "kind": "value",
          "declaration": "let toBullets : (List<string>) => string",
          "docs": "The strings as a bulleted block, one item per line."
        },
        {
          "name": "split",
          "qualified_name": "Text.split",
          "kind": "value",
          "declaration": "let split : (string, string) => List<string>",
          "docs": "Split the subject on every occurrence of `sep`. Splitting the empty string\ngives `[\"\"]`; an empty separator is an error."
        },
        {
          "name": "join",
          "qualified_name": "Text.join",
          "kind": "value",
          "declaration": "let join : (string, List<string>) => string",
          "docs": "Join the strings with `sep` between them. An empty separator is fine here —\nunlike `Text.split` — and simply concatenates."
        },
        {
          "name": "parseFloat",
          "qualified_name": "Text.parseFloat",
          "kind": "value",
          "declaration": "let parseFloat : (string) => float",
          "docs": "Parse a number, ignoring surrounding whitespace. Unparseable text answers\n`0.0` rather than raising — validate the input yourself when the difference\nmatters. Text that parses to a non-finite number (`\"inf\"`, `\"NaN\"`, an\noverflowing literal) degrades to `0.0` as well, so the result is always\nfinite."
        },
        {
          "name": "length",
          "qualified_name": "Text.length",
          "kind": "value",
          "declaration": "let length : (string) => float",
          "docs": "How many Unicode scalar values the string contains."
        },
        {
          "name": "chars",
          "qualified_name": "Text.chars",
          "kind": "value",
          "declaration": "let chars : (string) => List<string>",
          "docs": "The string's Unicode scalar values, each as a one-character string."
        },
        {
          "name": "toUpper",
          "qualified_name": "Text.toUpper",
          "kind": "value",
          "declaration": "let toUpper : (string) => string",
          "docs": "The string uppercased, Unicode-aware — so the LENGTH may change (`\"ß\"`\nuppercases to `\"SS\"`)."
        },
        {
          "name": "toLower",
          "qualified_name": "Text.toLower",
          "kind": "value",
          "declaration": "let toLower : (string) => string",
          "docs": "The string lowercased, Unicode-aware — so the length may change."
        },
        {
          "name": "trim",
          "qualified_name": "Text.trim",
          "kind": "value",
          "declaration": "let trim : (string) => string",
          "docs": "The string without leading or trailing whitespace."
        },
        {
          "name": "contains",
          "qualified_name": "Text.contains",
          "kind": "value",
          "declaration": "let contains : (string, string) => bool",
          "docs": "Whether the subject contains `needle`. The empty needle is contained in\nevery string."
        },
        {
          "name": "replace",
          "qualified_name": "Text.replace",
          "kind": "value",
          "declaration": "let replace : (string, string, string) => string",
          "docs": "Replace EVERY occurrence of `from` with `to`, never re-scanning what was\njust written. An empty `from` is an error."
        }
      ]
    },
    {
      "name": "Math",
      "group": "stdlib",
      "category": "Numbers & randomness",
      "docs": "Numeric functions and constants.\n\nFunctor Lang has one number type (`float`, an f64) and a deliberately small\noperator set: `Math.mod` stands in for `%` and `Math.pow` for `^`.\nArithmetic is IEEE throughout, so `1.0 / 0.0` is infinity and NaN compares\nfalse against everything, itself included.\n\nTwo behaviors differ from the usual defaults and are worth knowing: `mod`\nis EUCLIDEAN (its result is never negative) and `round` goes half AWAY FROM\nZERO (not banker's rounding).\n\nUnlike the collections, most of `Math` reads as ordinary notation rather\nthan as a pipeline: `pow`, `atan2`, `mod`, `min`, and `max` take their\nNUMBER FIRST, so for the order-sensitive ones (`pow`, `atan2`, `mod`)\npiping feeds the wrong slot. `clamp`, `clamp01`, `lerp`, and `smoothstep`\nare the deliberate subject-last exceptions — their bounds and parameters\nare configuration and their number is the subject, so they pipe:\n`n |> Math.clamp(0.0, 10.0)`.",
      "items": [
        {
          "name": "pi",
          "qualified_name": "Math.pi",
          "kind": "value",
          "declaration": "let pi : float",
          "docs": "The ratio of a circle's circumference to its diameter — a constant VALUE,\nnot a function, so it is written `Math.pi` with no parentheses."
        },
        {
          "name": "sin",
          "qualified_name": "Math.sin",
          "kind": "value",
          "declaration": "let sin : (float) => float",
          "docs": "The sine of an angle in radians."
        },
        {
          "name": "cos",
          "qualified_name": "Math.cos",
          "kind": "value",
          "declaration": "let cos : (float) => float",
          "docs": "The cosine of an angle in radians."
        },
        {
          "name": "tan",
          "qualified_name": "Math.tan",
          "kind": "value",
          "declaration": "let tan : (float) => float",
          "docs": "The tangent of an angle in radians."
        },
        {
          "name": "asin",
          "qualified_name": "Math.asin",
          "kind": "value",
          "declaration": "let asin : (float) => float",
          "docs": "The arcsine, in radians. NaN outside `[-1, 1]`: it does NOT clamp, so a dot\nproduct nudged past 1.0 by float error needs an explicit\n`|> Math.clamp(-1.0, 1.0)` first."
        },
        {
          "name": "acos",
          "qualified_name": "Math.acos",
          "kind": "value",
          "declaration": "let acos : (float) => float",
          "docs": "The arccosine, in radians. NaN outside `[-1, 1]`, exactly like `asin`."
        },
        {
          "name": "atan",
          "qualified_name": "Math.atan",
          "kind": "value",
          "declaration": "let atan : (float) => float",
          "docs": "The arctangent, in radians."
        },
        {
          "name": "atan2",
          "qualified_name": "Math.atan2",
          "kind": "value",
          "declaration": "let atan2 : (float, float) => float",
          "docs": "The angle in radians from the positive x-axis to the point `(x, y)`, using\nthe standard mathematical argument order with `y` FIRST."
        },
        {
          "name": "sqrt",
          "qualified_name": "Math.sqrt",
          "kind": "value",
          "declaration": "let sqrt : (float) => float",
          "docs": "The non-negative square root."
        },
        {
          "name": "pow",
          "qualified_name": "Math.pow",
          "kind": "value",
          "declaration": "let pow : (float, float) => float",
          "docs": "`base` raised to `exp` — the language has no `^` operator."
        },
        {
          "name": "log",
          "qualified_name": "Math.log",
          "kind": "value",
          "declaration": "let log : (float) => float",
          "docs": "The natural logarithm, base e — the inverse of `Math.exp`."
        },
        {
          "name": "exp",
          "qualified_name": "Math.exp",
          "kind": "value",
          "declaration": "let exp : (float) => float",
          "docs": "e raised to the given power."
        },
        {
          "name": "abs",
          "qualified_name": "Math.abs",
          "kind": "value",
          "declaration": "let abs : (float) => float",
          "docs": "The magnitude, without sign."
        },
        {
          "name": "sign",
          "qualified_name": "Math.sign",
          "kind": "value",
          "declaration": "let sign : (float) => float",
          "docs": "`-1`, `0`, or `1` — and exactly `0` AT zero, so it is not a two-way branch.\nNaN answers NaN."
        },
        {
          "name": "floor",
          "qualified_name": "Math.floor",
          "kind": "value",
          "declaration": "let floor : (float) => float",
          "docs": "The largest whole number that is not greater than `n`."
        },
        {
          "name": "ceil",
          "qualified_name": "Math.ceil",
          "kind": "value",
          "declaration": "let ceil : (float) => float",
          "docs": "The smallest whole number that is not less than `n`."
        },
        {
          "name": "round",
          "qualified_name": "Math.round",
          "kind": "value",
          "declaration": "let round : (float) => float",
          "docs": "The nearest whole number, rounding halves AWAY FROM ZERO rather than to\neven: `0.5` rounds to `1`, `2.5` to `3`, and `-2.5` to `-3`."
        },
        {
          "name": "mod",
          "qualified_name": "Math.mod",
          "kind": "value",
          "declaration": "let mod : (float, float) => float",
          "docs": "The EUCLIDEAN remainder: the result always lands in `[0, abs(b))`, so\nnegatives wrap positively — `Math.mod(-1.0, 8.0)` is `7.0`, the wraparound\ngames want. A zero divisor answers NaN."
        },
        {
          "name": "min",
          "qualified_name": "Math.min",
          "kind": "value",
          "declaration": "let min : (float, float) => float",
          "docs": "The smaller of two numbers."
        },
        {
          "name": "max",
          "qualified_name": "Math.max",
          "kind": "value",
          "declaration": "let max : (float, float) => float",
          "docs": "The larger of two numbers."
        },
        {
          "name": "clamp",
          "qualified_name": "Math.clamp",
          "kind": "value",
          "declaration": "let clamp : (float, float, float) => float",
          "docs": "`n` confined to `[low, high]`, subject-last so it pipes:\n`n |> Math.clamp(0.0, 10.0)`. A `low` greater than `high` is an error, not\na silent swap."
        },
        {
          "name": "clamp01",
          "qualified_name": "Math.clamp01",
          "kind": "value",
          "declaration": "let clamp01 : (float) => float",
          "docs": "`n` confined to `[0, 1]` — the same as `Math.clamp(0.0, 1.0, n)`."
        },
        {
          "name": "lerp",
          "qualified_name": "Math.lerp",
          "kind": "value",
          "declaration": "let lerp : (float, float, float) => float",
          "docs": "Linear interpolation from `from` toward `target` by `t` (unclamped):\n`from + (target - from) * t`, and `t = 1.0` answers `target` exactly.\nMirrors `Vec3.lerp(target, t, from)` — the pipe subject is the start\nvalue, so `x |> Math.lerp(target, t)` works like its vector sibling.\n(Deliberately NOT GLSL's `mix(a, b, t)` order.)"
        },
        {
          "name": "smoothstep",
          "qualified_name": "Math.smoothstep",
          "kind": "value",
          "declaration": "let smoothstep : (float, float, float) => float",
          "docs": "Hermite smoothstep of `x` across `[edge0, edge1]`, clamped to `[0, 1]`.\nThe edges must be a finite ascending range (`edge0 < edge1`, both finite\nand not overflow-wide) — anything else is an error, never a silent NaN."
        }
      ]
    },
    {
      "name": "Random",
      "group": "stdlib",
      "category": "Numbers & randomness",
      "docs": "Pure, seeded pseudo-random numbers.\n\nThere is no hidden global generator: a draw takes a seed and hands back the\nNEXT seed alongside its value, so randomness is an ordinary part of the\nmodel. Thread the next seed through and a run is exactly reproducible —\nwhich is what makes rewind, replay, and hot reload work.\n\nSeed a stream once, at `init`: a fixed `Random.seed(42.0)` for a\nreproducible run, or `Effect.random` / `Effect.now` for a different stream\neach session. Distinct seeds produce DECORRELATED streams that share no\nprefix, so per-entity streams do not visibly rhyme with each other.",
      "items": [
        {
          "name": "Seed",
          "qualified_name": "Random.Seed",
          "kind": "type",
          "declaration": "type Seed",
          "docs": "An opaque PRNG seed.\n\nThe brand keeps seeds out of arithmetic: a bare number where a `Seed` is\nexpected, or `seed + 1.0` to derive a sibling stream, is a check-time error\n— use `Random.fork` instead. At runtime a seed is still plain data, so it\nsnapshots, hot-reloads, and time-travels like any other model field."
        },
        {
          "name": "seed",
          "qualified_name": "Random.seed",
          "kind": "value",
          "declaration": "let seed : (float) => Seed",
          "docs": "Make a seed from any finite number.\n\nThe number's BITS are hashed, so fractional seeds are as usable as whole\nones — `Random.seed(0.42)` from an `Effect.random` result names a distinct\nstarting point just as `Random.seed(42.0)` does."
        },
        {
          "name": "step",
          "qualified_name": "Random.step",
          "kind": "value",
          "declaration": "let step : (Seed) => (float, Seed)",
          "docs": "Draw the next value, as `(value, nextSeed)` with `value` in `[0, 1)`.\n\nThe same seed always yields the same pair. Bind both halves and carry the\nnext seed forward — `let (v, next) = Random.step(model.seed) in …` —\notherwise the stream never advances."
        },
        {
          "name": "range",
          "qualified_name": "Random.range",
          "kind": "value",
          "declaration": "let range : (float, float, Seed) => (float, Seed)",
          "docs": "Draw one value rescaled into `[lo, hi)`, as `(value, nextSeed)` — one\n`step` draw, interpolated between the bounds.\n\nOnly finiteness is checked, so the bounds are yours to get right: reversed\nones simply interpolate the other way (landing in `(hi, lo]`), and equal\nones always answer that value."
        },
        {
          "name": "fork",
          "qualified_name": "Random.fork",
          "kind": "value",
          "declaration": "let fork : (float, Seed) => Seed",
          "docs": "The seed of decorrelated child stream `i` — the typed replacement for\nderiving sibling streams by arithmetic. Subject-last, so per-entity streams\nread as `model.seed |> Random.fork(i)`, and any number may name a stream."
        }
      ]
    },
    {
      "name": "Option",
      "group": "stdlib",
      "category": "Fallibility",
      "docs": "A value that may be absent.\n\n`Option` is an ordinary generic variant bundled with the language, so it is\navailable in every project and under the plain `functor-lang` CLI. It is\nwhat the partial accessors (`List.head`, `Map.get`, …) answer with instead\nof a sentinel, which is what forces the absent case to be handled.\n\nALWAYS qualify the constructors: bare `Some` / `None` do not resolve — they\nbelong to this module, so write `Option.Some(x)` and `Option.None` in both\nexpressions and patterns (or `open Option` first).\n\nHelpers take the option LAST, so they thread through a pipeline:\n`Option.Some(41.0) |> Option.map((n) => n + 1.0) |> Option.defaultValue(0.0)`.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Option.t",
          "kind": "type",
          "declaration": "type t<'value> =\n  | Some(value: 'value)\n  | None",
          "docs": "A present value (`Option.Some`) or its absence (`Option.None`)."
        },
        {
          "name": "map",
          "qualified_name": "Option.map",
          "kind": "value",
          "declaration": "let map : (('value) => 'mapped, t<'value>) => t<'mapped>",
          "docs": "Transform a present value; leave `None` unchanged."
        },
        {
          "name": "bind",
          "qualified_name": "Option.bind",
          "kind": "value",
          "declaration": "let bind : (('value) => t<'mapped>, t<'value>) => t<'mapped>",
          "docs": "Continue with an optional computation when a value is present."
        },
        {
          "name": "defaultValue",
          "qualified_name": "Option.defaultValue",
          "kind": "value",
          "declaration": "let defaultValue : ('value, t<'value>) => 'value",
          "docs": "Return the contained value, or an eager fallback for `None`."
        },
        {
          "name": "defaultWith",
          "qualified_name": "Option.defaultWith",
          "kind": "value",
          "declaration": "let defaultWith : (() => 'value, t<'value>) => 'value",
          "docs": "Return the contained value, computing the fallback only for `None`."
        },
        {
          "name": "isSome",
          "qualified_name": "Option.isSome",
          "kind": "value",
          "declaration": "let isSome : (t<'value>) => bool",
          "docs": "Whether the option contains a value."
        },
        {
          "name": "isNone",
          "qualified_name": "Option.isNone",
          "kind": "value",
          "declaration": "let isNone : (t<'value>) => bool",
          "docs": "Whether the option is `None`."
        },
        {
          "name": "filter",
          "qualified_name": "Option.filter",
          "kind": "value",
          "declaration": "let filter : (('value) => bool, t<'value>) => t<'value>",
          "docs": "Keep a present value only when it satisfies the predicate."
        },
        {
          "name": "toList",
          "qualified_name": "Option.toList",
          "kind": "value",
          "declaration": "let toList : (t<'value>) => List<'value>",
          "docs": "Convert `Some(value)` to `[value]` and `None` to `[]`."
        }
      ]
    },
    {
      "name": "Result",
      "group": "stdlib",
      "category": "Fallibility",
      "docs": "A computation that either succeeded or carries an error.\n\n`Result` is an ordinary generic variant bundled with the language, so it is\navailable in every project and under the plain `functor-lang` CLI. Use it\nwhere a failure needs to explain itself; use `Option` where absence needs\nno explanation.\n\nALWAYS qualify the constructors: bare `Ok` / `Error` do not resolve — write\n`Result.Ok(x)` and `Result.Error(e)` in both expressions and patterns (or\n`open Result` first).\n\nHelpers take the result LAST, so they thread through a pipeline.",
      "items": [
        {
          "name": "t",
          "qualified_name": "Result.t",
          "kind": "type",
          "declaration": "type t<'value, 'error> =\n  | Ok(value: 'value)\n  | Error(error: 'error)",
          "docs": "A success carrying a value (`Result.Ok`) or a failure carrying an error\n(`Result.Error`)."
        },
        {
          "name": "map",
          "qualified_name": "Result.map",
          "kind": "value",
          "declaration": "let map : (('value) => 'mapped, t<'value, 'error>) => t<'mapped, 'error>",
          "docs": "Transform a successful value; leave an error unchanged."
        },
        {
          "name": "mapError",
          "qualified_name": "Result.mapError",
          "kind": "value",
          "declaration": "let mapError : (('error) => 'mapped, t<'value, 'error>) => t<'value, 'mapped>",
          "docs": "Transform an error; leave a successful value unchanged."
        },
        {
          "name": "bind",
          "qualified_name": "Result.bind",
          "kind": "value",
          "declaration": "let bind : (('value) => t<'mapped, 'error>, t<'value, 'error>) => t<'mapped, 'error>",
          "docs": "Continue with a result-producing computation after success."
        },
        {
          "name": "defaultValue",
          "qualified_name": "Result.defaultValue",
          "kind": "value",
          "declaration": "let defaultValue : ('value, t<'value, 'error>) => 'value",
          "docs": "Return the successful value, or an eager fallback for an error."
        },
        {
          "name": "defaultWith",
          "qualified_name": "Result.defaultWith",
          "kind": "value",
          "declaration": "let defaultWith : (('error) => 'value, t<'value, 'error>) => 'value",
          "docs": "Return the successful value, or compute a fallback from the error."
        },
        {
          "name": "isOk",
          "qualified_name": "Result.isOk",
          "kind": "value",
          "declaration": "let isOk : (t<'value, 'error>) => bool",
          "docs": "Whether the result is successful."
        },
        {
          "name": "isError",
          "qualified_name": "Result.isError",
          "kind": "value",
          "declaration": "let isError : (t<'value, 'error>) => bool",
          "docs": "Whether the result contains an error."
        },
        {
          "name": "toOption",
          "qualified_name": "Result.toOption",
          "kind": "value",
          "declaration": "let toOption : (t<'value, 'error>) => Option.t<'value>",
          "docs": "Convert `Ok(value)` to `Option.Some(value)` and an error to `Option.None`."
        }
      ]
    },
    {
      "name": "Key",
      "group": "stdlib",
      "category": "Input",
      "docs": "Keyboard keys, as a variant rather than strings.\n\n`Key` is built in — no declaration and no import. The `input` hook's `key`\nparameter and the `Input.snapshot` key sets all carry these constructors,\nso a misspelling (`Key.Enterr`) is a load-time error instead of an arm that\nsilently never matches. Match them (`| Key.W =>`) or compare them\n(`key == Key.Enter`).",
      "items": [
        {
          "name": "t",
          "qualified_name": "Key.t",
          "kind": "type",
          "declaration": "type t =\n  | A | B | C | D | E | F | G | H | I | J | K | L | M\n  | N | O | P | Q | R | S | T | U | V | W | X | Y | Z\n  | Up | Down | Left | Right\n  | Space | Enter | Escape\n  | Num0 | Num1 | Num2 | Num3 | Num4 | Num5 | Num6 | Num7 | Num8 | Num9",
          "docs": "A keyboard key.\n\nThe digit row is `Num0` … `Num9` — constructor names have to be\nidentifiers, so a bare digit is not one of them. Keys the platform reports\nthat Functor does not name are never delivered to game logic."
        }
      ]
    },
    {
      "name": "Mouse",
      "group": "stdlib",
      "category": "Input",
      "docs": "Mouse buttons, as a variant rather than strings.\n\nThe mouse twin of `Key`, and built in the same way: the `mouseButton`\nhook's `button` parameter carries these constructors, so a typo is caught\nat load time. Match them (`| Mouse.Left =>`) or compare them\n(`button == Mouse.Right`).",
      "items": [
        {
          "name": "t",
          "qualified_name": "Mouse.t",
          "kind": "type",
          "declaration": "type t =\n  | Left | Right | Middle",
          "docs": "A mouse button. Buttons beyond these three are never delivered to game\nlogic."
        }
      ]
    },
    {
      "name": "Debug",
      "group": "stdlib",
      "category": "Diagnostics",
      "docs": "The observability escape hatch.\n\n`Debug` is the one impure corner of the standard library, and it is impure\nonly in the direction of the terminal: it cannot influence the model or the\nsimulation, so a game with and without it evolves identically.",
      "items": [
        {
          "name": "log",
          "qualified_name": "Debug.log",
          "kind": "value",
          "declaration": "let log : (string, 'a) => 'a",
          "docs": "Log `label: value` and return `value` UNCHANGED — an Elm-style trace.\n\nThe value is rendered exactly as `functor-lang run` displays it, whatever\nits type. Because the label comes first and the subject last, it reads\nstandalone (`Debug.log(\"x\", model.x)`) and threads through a pipeline\n(`model.x |> Debug.log(\"x\") |> Math.clamp01`) without changing what flows\nthrough.\n\nUnder the plain `functor-lang` CLI the line goes to stdout; under the game\nrunner it goes to the CLI's log stream, or the browser console on wasm. It\nis NOT rate-limited, so a call in `tick` or `draw` fires every frame —\nprefer an event path such as `input` or `update`, or remove the call when\nyou are done with it."
        }
      ]
    }
  ]
}
