{
  "openapi": "3.1.0",
  "info": {
    "title": "Construction Calculator API",
    "version": "1.0.0",
    "summary": "60+ contractor-grade material and cost calculators: roofing, decking, fencing, drywall, insulation, tile, gravel, concrete, paint.",
    "description": "Material take-offs and planning costs as JSON — the same tested formulas that power the Smart Calculator family of sites (smartroofingcalculator.com, smartdeckingcalculator.com, smartfencingcalculator.com, smarttilecalculator.com, smartgravelcalculator.com). Every endpoint accepts GET with query parameters or POST with a JSON body, supports `units=imperial|metric`, validates every input, and returns `result`, `units_of_measure` (unit label for each field), `notes` and `warnings`. Fields whose name carries a unit (volumeCubicFeet, weightPounds, tonsToOrder) are always in that unit. Errors use one shape: `{ error: { status, code, message, details[] }, requestId }`.",
    "contact": {
      "name": "Dixon App Studio",
      "url": "https://www.dixonappstudio.com"
    },
    "license": {
      "name": "Proprietary",
      "identifier": "LicenseRef-Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://construction-calc-api.netlify.app",
      "description": "Production (reached through RapidAPI)"
    }
  ],
  "tags": [
    {
      "name": "Roofing",
      "description": "Pitch, area, shingles, underlayment, drip edge, ridge cap, starter, nails, sheathing, flashing, valleys, ventilation, gutters, trusses, metal/flat/tile roofs, coating, snow load, labor, solar."
    },
    {
      "name": "Decking",
      "description": "Deck cost, boards, joists, screws, posts, footings, balusters, cable railing, stain."
    },
    {
      "name": "Fencing",
      "description": "Posts, pickets, rails, post concrete, panels, chain-link, electric fence, acreage, stain."
    },
    {
      "name": "Drywall",
      "description": "Sheets, full room takeoff, mud/tape/bead, screws."
    },
    {
      "name": "Insulation",
      "description": "Blown-in bags to a target R-value, wall batts, thickness for an R-value."
    },
    {
      "name": "Tile",
      "description": "Tile count and boxes, grout, thinset, backer board, cost."
    },
    {
      "name": "Landscape & Aggregates",
      "description": "Gravel, mulch, topsoil, sand, sod, grass seed."
    },
    {
      "name": "Concrete",
      "description": "Slabs with gravel base, footings/columns/holes → yards and bags."
    },
    {
      "name": "Paint",
      "description": "Paint and primer for a room."
    },
    {
      "name": "Conversions",
      "description": "Unit conversion, pitch/degrees/percent, board feet, area × depth → volume."
    },
    {
      "name": "Meta",
      "description": "Health and catalog."
    }
  ],
  "paths": {
    "/v1/roofing/pitch": {
      "get": {
        "operationId": "roofingPitch",
        "tags": [
          "Roofing"
        ],
        "summary": "Roof pitch → angle, slope %, pitch factor, ridge height",
        "description": "Converts a rise/run pitch into degrees, percent slope and the pitch factor (slope multiplier used to turn a flat footprint into true roof area). Give a span to also get the ridge height.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "rise",
            "in": "query",
            "required": true,
            "description": "Rise in inches per `run` inches.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 48
            },
            "example": 6
          },
          {
            "name": "run",
            "in": "query",
            "required": false,
            "description": "Run in inches (usually 12). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 48,
              "default": 12
            }
          },
          {
            "name": "span",
            "in": "query",
            "required": false,
            "description": "Building span (wall to wall) — optional, for ridge height. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 30
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/pitch",
                  "units": "imperial",
                  "input": {
                    "rise": 6,
                    "run": 12,
                    "span": 30
                  },
                  "result": {
                    "pitch": "6/12",
                    "angleDeg": 26.5651,
                    "slopePct": 50,
                    "pitchFactor": 1.118,
                    "ridgeHeight": 7.5
                  },
                  "units_of_measure": {
                    "angleDeg": "°",
                    "slopePct": "%",
                    "ridgeHeight": "ft"
                  },
                  "notes": [
                    "Moderate slope — standard shingles suitable."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/area": {
      "get": {
        "operationId": "roofingArea",
        "tags": [
          "Roofing"
        ],
        "summary": "Roof area and squares from a footprint",
        "description": "Slope-adjusted roof area from the building footprint: (length + overhang) × (width + overhang) × sections × pitch factor. One roofing square = 100 sq ft.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Footprint length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 50
          },
          {
            "name": "width",
            "in": "query",
            "required": true,
            "description": "Footprint width. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 40
          },
          {
            "name": "pitch",
            "in": "query",
            "required": false,
            "description": "Roof pitch as rise in inches per 12 in of run (6 = a 6/12 roof). Ignored when pitchFactor is given.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 36,
              "default": 6
            },
            "example": 6
          },
          {
            "name": "pitchFactor",
            "in": "query",
            "required": false,
            "description": "Slope multiplier to apply directly (e.g. 1.118 for 6/12). Overrides pitch.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 4
            }
          },
          {
            "name": "overhang",
            "in": "query",
            "required": false,
            "description": "Eave overhang added to each dimension. Unit: in (imperial) or mm (metric). Max 48 in (1219.2 mm). Default 0 in (≈0 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "sections",
            "in": "query",
            "required": false,
            "description": "Number of identical roof sections.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 1
            }
          },
          {
            "name": "costPerSquare",
            "in": "query",
            "required": false,
            "description": "Installed cost per square (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/area",
                  "units": "imperial",
                  "input": {
                    "length": 50,
                    "width": 40,
                    "pitch": 6,
                    "overhang": 0,
                    "sections": 1
                  },
                  "result": {
                    "pitchFactor": 1.118,
                    "footprintArea": 2000,
                    "roofArea": 2236.068,
                    "squares": 22.3607,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "footprintArea": "sq ft",
                    "roofArea": "sq ft"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/shingles": {
      "get": {
        "operationId": "roofingShingles",
        "tags": [
          "Roofing"
        ],
        "summary": "Shingle bundles and squares",
        "description": "Bundles of asphalt shingles for a roof: footprint × sections × pitch factor, plus waste, ÷ coverage per bundle (33.3 sq ft = 3 bundles per square). Bundles are rounded up with a float-safe ceiling so an exact multiple never orders an extra bundle.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Footprint length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 50
          },
          {
            "name": "width",
            "in": "query",
            "required": true,
            "description": "Footprint width. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 40
          },
          {
            "name": "pitch",
            "in": "query",
            "required": false,
            "description": "Roof pitch as rise in inches per 12 in of run (6 = a 6/12 roof). Ignored when pitchFactor is given.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 36,
              "default": 6
            },
            "example": 6
          },
          {
            "name": "pitchFactor",
            "in": "query",
            "required": false,
            "description": "Slope multiplier to apply directly (e.g. 1.118 for 6/12). Overrides pitch.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 4
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance (10% gable, 15% hip, 20% complex). Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "coverage",
            "in": "query",
            "required": false,
            "description": "Coverage per bundle. Unit: sq ft (imperial) or m² (metric). Must be greater than 0. Max 100 sq ft (9.29 m²). Default 33.3 sq ft (≈3.094 m²).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 33.3
            }
          },
          {
            "name": "sections",
            "in": "query",
            "required": false,
            "description": "Number of identical roof sections.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 1
            }
          },
          {
            "name": "bundleCost",
            "in": "query",
            "required": false,
            "description": "Price per bundle (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/shingles",
                  "units": "imperial",
                  "input": {
                    "length": 50,
                    "width": 40,
                    "pitch": 6,
                    "wastePercent": 10,
                    "coverage": 33.3,
                    "sections": 1
                  },
                  "result": {
                    "footprintArea": 2000,
                    "roofArea": 2236.068,
                    "wasteArea": 223.6068,
                    "totalArea": 2459.6748,
                    "squares": 24.5967,
                    "bundlesExact": 73.8641,
                    "bundles": 74,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "footprintArea": "sq ft",
                    "roofArea": "sq ft",
                    "wasteArea": "sq ft",
                    "totalArea": "sq ft",
                    "bundles": "count"
                  },
                  "notes": [
                    "Average residential roof size."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/underlayment": {
      "get": {
        "operationId": "roofingUnderlayment",
        "tags": [
          "Roofing"
        ],
        "summary": "Underlayment rolls",
        "description": "Rolls of felt or synthetic underlayment: area × pitch factor × sections × (1 + waste) ÷ roll coverage (synthetic ≈ 400–1,000 sq ft; #15 felt ≈ 400; #30 felt ≈ 200).\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Footprint (plan) area, or true roof area with pitchFactor=1. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 2000
          },
          {
            "name": "pitch",
            "in": "query",
            "required": false,
            "description": "Roof pitch as rise in inches per 12 in of run (6 = a 6/12 roof). Ignored when pitchFactor is given.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 36,
              "default": 6
            },
            "example": 6
          },
          {
            "name": "pitchFactor",
            "in": "query",
            "required": false,
            "description": "Slope multiplier to apply directly (e.g. 1.118 for 6/12). Overrides pitch.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 4
            }
          },
          {
            "name": "coverage",
            "in": "query",
            "required": false,
            "description": "Coverage per roll. Unit: sq ft (imperial) or m² (metric). Must be greater than 0. Default 400 sq ft (≈37.161 m²).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 400
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Overlap and waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "sections",
            "in": "query",
            "required": false,
            "description": "Identical sections.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 1
            }
          },
          {
            "name": "rollCost",
            "in": "query",
            "required": false,
            "description": "Price per roll (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/underlayment",
                  "units": "imperial",
                  "input": {
                    "area": 2000,
                    "pitch": 6,
                    "coverage": 400,
                    "wastePercent": 10,
                    "sections": 1
                  },
                  "result": {
                    "totalArea": 2459.6748,
                    "rolls": 7,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "totalArea": "sq ft",
                    "rolls": "count"
                  },
                  "notes": [
                    "Synthetic underlayment tolerates UV exposure and tears less than felt."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/drip-edge": {
      "get": {
        "operationId": "roofingDripEdge",
        "tags": [
          "Roofing"
        ],
        "summary": "Drip edge pieces",
        "description": "Drip edge for the full perimeter 2 × (length + width) plus waste, in stock pieces (10 ft default).\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Roof length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 50
          },
          {
            "name": "width",
            "in": "query",
            "required": true,
            "description": "Roof width. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 40
          },
          {
            "name": "pieceLength",
            "in": "query",
            "required": false,
            "description": "Length of one drip-edge piece. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 20 ft (6.096 m). Default 10 ft (≈3.048 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 10
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Overlap and waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "pieceCost",
            "in": "query",
            "required": false,
            "description": "Price per piece (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/drip-edge",
                  "units": "imperial",
                  "input": {
                    "length": 50,
                    "width": 40,
                    "pieceLength": 10,
                    "wastePercent": 10
                  },
                  "result": {
                    "perimeter": 180,
                    "totalLength": 198,
                    "pieces": 20,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "perimeter": "ft",
                    "totalLength": "ft",
                    "pieces": "count"
                  },
                  "notes": [
                    "Use Type D at eaves and Type F at rakes."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/ridge-cap": {
      "get": {
        "operationId": "roofingRidgeCap",
        "tags": [
          "Roofing"
        ],
        "summary": "Hip & ridge cap bundles",
        "description": "Ridge cap bundles: (ridge + hip length) × (1 + waste) ÷ linear feet per bundle (≈33.3 LF), float-safe rounded up.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "ridgeLength",
            "in": "query",
            "required": true,
            "description": "Total ridge plus hip length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 100
          },
          {
            "name": "coverage",
            "in": "query",
            "required": false,
            "description": "Linear coverage per bundle. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 100 ft (30.48 m). Default 33.3 ft (≈10.15 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 33.3
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 0
            }
          },
          {
            "name": "bundleCost",
            "in": "query",
            "required": false,
            "description": "Price per bundle (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/ridge-cap",
                  "units": "imperial",
                  "input": {
                    "ridgeLength": 100,
                    "coverage": 33.3,
                    "wastePercent": 0
                  },
                  "result": {
                    "totalLength": 100,
                    "bundles": 4,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "totalLength": "ft",
                    "bundles": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/starter-strip": {
      "get": {
        "operationId": "roofingStarterStrip",
        "tags": [
          "Roofing"
        ],
        "summary": "Starter strip bundles",
        "description": "Starter shingles along eaves and rakes: (eave + rake) × (1 + waste) ÷ linear feet per bundle (105 LF default).\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "eaveLength",
            "in": "query",
            "required": true,
            "description": "Total eave length. Unit: ft (imperial) or m (metric).",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 100
          },
          {
            "name": "rakeLength",
            "in": "query",
            "required": false,
            "description": "Total rake length (0 to skip rakes). Unit: ft (imperial) or m (metric). Default 0 ft (≈0 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0
            },
            "example": 80
          },
          {
            "name": "coverage",
            "in": "query",
            "required": false,
            "description": "Linear feet per bundle. Unit: ft (imperial) or m (metric). Must be greater than 0. Default 105 ft (≈32.004 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 105
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance (0 is treated as the site default of 5%). Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 5
            }
          },
          {
            "name": "bundleCost",
            "in": "query",
            "required": false,
            "description": "Price per bundle (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/starter-strip",
                  "units": "imperial",
                  "input": {
                    "eaveLength": 100,
                    "rakeLength": 80,
                    "coverage": 105,
                    "wastePercent": 5
                  },
                  "result": {
                    "edgeLength": 180,
                    "totalLength": 189,
                    "bundles": 2,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "edgeLength": "ft",
                    "totalLength": "ft",
                    "bundles": "count"
                  },
                  "notes": [
                    "Use self-seal starter at eaves and rakes for wind resistance."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/nails": {
      "get": {
        "operationId": "roofingNails",
        "tags": [
          "Roofing"
        ],
        "summary": "Roofing nails (count and pounds)",
        "description": "Coil/hand nails: squares × shingles per square (66 architectural, 80 three-tab) × nails per shingle; ~140 1¼\" roofing nails per lb.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "True roof area (after pitch). Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 2000
          },
          {
            "name": "nailsPerShingle",
            "in": "query",
            "required": false,
            "description": "Nails per shingle (4 standard, 6 high-wind).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12,
              "default": 4
            }
          },
          {
            "name": "shingleType",
            "in": "query",
            "required": false,
            "description": "Shingle type.",
            "schema": {
              "type": "string",
              "enum": [
                "architectural",
                "3-tab"
              ],
              "default": "architectural"
            }
          },
          {
            "name": "costPerLb",
            "in": "query",
            "required": false,
            "description": "Price per pound (optional). Unit: currency/lb (imperial) or currency/kg (metric).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/nails",
                  "units": "imperial",
                  "input": {
                    "area": 2000,
                    "nailsPerShingle": 4,
                    "shingleType": "architectural"
                  },
                  "result": {
                    "squares": 20,
                    "nails": 5280,
                    "weight": 37.7143,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "nails": "count",
                    "weight": "lb"
                  },
                  "notes": [
                    "Standard pattern; use 6 per shingle in high-wind zones."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/sheathing": {
      "get": {
        "operationId": "roofingSheathing",
        "tags": [
          "Roofing"
        ],
        "summary": "Plywood / OSB roof sheathing sheets",
        "description": "Sheets of roof decking: area × (1 + waste) ÷ sheet area, float-safe rounded up. Give a thickness to get weight (~80 lb per inch of thickness for a 4×8).\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "True roof area (after pitch). Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 2236
          },
          {
            "name": "sheetLength",
            "in": "query",
            "required": false,
            "description": "Sheet length. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 12 ft (3.658 m). Default 8 ft (≈2.438 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 8
            }
          },
          {
            "name": "sheetWidth",
            "in": "query",
            "required": false,
            "description": "Sheet width. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 8 ft (2.438 m). Default 4 ft (≈1.219 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 4
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "thickness",
            "in": "query",
            "required": false,
            "description": "Panel thickness (optional, for weight). Unit: in (imperial) or mm (metric). Must be greater than 0. Max 2 in (50.8 mm).",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 0.5
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/sheathing",
                  "units": "imperial",
                  "input": {
                    "area": 2236,
                    "sheetLength": 8,
                    "sheetWidth": 4,
                    "wastePercent": 10,
                    "thickness": 0.5
                  },
                  "result": {
                    "areaWithWaste": 2459.6,
                    "sheetArea": 32,
                    "sheets": 77,
                    "totalWeight": 3080
                  },
                  "units_of_measure": {
                    "areaWithWaste": "sq ft",
                    "sheetArea": "sq ft",
                    "sheets": "count",
                    "totalWeight": "lb"
                  },
                  "notes": [
                    "Large job — schedule boom delivery and a staging area."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/flashing": {
      "get": {
        "operationId": "roofingFlashing",
        "tags": [
          "Roofing"
        ],
        "summary": "Flashing linear feet, pieces and material cost ranges",
        "description": "Sums edge, wall, valley, skylight, dormer and extra flashing runs, adds waste, and returns 10 ft pieces plus galvanized/aluminum/copper cost ranges.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "edges",
            "in": "query",
            "required": false,
            "description": "Edge flashing length. Unit: ft (imperial) or m (metric). Default 0 ft (≈0 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0
            },
            "example": 60
          },
          {
            "name": "wall",
            "in": "query",
            "required": false,
            "description": "Wall/step flashing length. Unit: ft (imperial) or m (metric). Default 0 ft (≈0 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0
            },
            "example": 20
          },
          {
            "name": "valley",
            "in": "query",
            "required": false,
            "description": "Valley flashing length. Unit: ft (imperial) or m (metric). Default 0 ft (≈0 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "skylight",
            "in": "query",
            "required": false,
            "description": "Skylight flashing length. Unit: ft (imperial) or m (metric). Default 0 ft (≈0 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "dormer",
            "in": "query",
            "required": false,
            "description": "Dormer flashing length. Unit: ft (imperial) or m (metric). Default 0 ft (≈0 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "extra",
            "in": "query",
            "required": false,
            "description": "Any other flashing length. Unit: ft (imperial) or m (metric). Default 0 ft (≈0 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "costPerFoot",
            "in": "query",
            "required": false,
            "description": "Your price per foot (optional). Unit: currency/ft (imperial) or currency/m (metric).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/flashing",
                  "units": "imperial",
                  "input": {
                    "edges": 60,
                    "wall": 20,
                    "valley": 0,
                    "skylight": 0,
                    "dormer": 0,
                    "extra": 0,
                    "wastePercent": 10
                  },
                  "result": {
                    "rawLength": 80,
                    "totalLength": 88,
                    "pieces10ft": 9,
                    "estimatedCost": null,
                    "costRanges": {
                      "galvanized": {
                        "low": 88,
                        "high": 176
                      },
                      "aluminum": {
                        "low": 176,
                        "high": 264
                      },
                      "copper": {
                        "low": 440,
                        "high": 1056
                      }
                    }
                  },
                  "units_of_measure": {
                    "rawLength": "ft",
                    "totalLength": "ft",
                    "pieces10ft": "count",
                    "costRanges.galvanized.low": "currency",
                    "costRanges.galvanized.high": "currency",
                    "costRanges.aluminum.low": "currency",
                    "costRanges.aluminum.high": "currency",
                    "costRanges.copper.low": "currency",
                    "costRanges.copper.high": "currency"
                  },
                  "notes": [
                    "Use step flashing at wall intersections."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/valleys": {
      "get": {
        "operationId": "roofingValleys",
        "tags": [
          "Roofing"
        ],
        "summary": "Valley flashing / ice-and-water",
        "description": "Valley material: count × length × pitch factor plus overlap per valley, with waste, in 10 ft pieces.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "valleyCount",
            "in": "query",
            "required": true,
            "description": "Number of valleys.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "example": 2
          },
          {
            "name": "valleyLength",
            "in": "query",
            "required": true,
            "description": "Plan length of each valley. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 20
          },
          {
            "name": "pitch",
            "in": "query",
            "required": false,
            "description": "Roof pitch as rise in inches per 12 in of run (6 = a 6/12 roof). Ignored when pitchFactor is given.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 36,
              "default": 6
            },
            "example": 6
          },
          {
            "name": "pitchFactor",
            "in": "query",
            "required": false,
            "description": "Slope multiplier to apply directly (e.g. 1.118 for 6/12). Overrides pitch.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 4
            }
          },
          {
            "name": "overlapAllowance",
            "in": "query",
            "required": false,
            "description": "Extra length per valley for laps. Unit: ft (imperial) or m (metric). Default 0 ft (≈0 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 0
            }
          },
          {
            "name": "costPerFoot",
            "in": "query",
            "required": false,
            "description": "Price per foot (optional). Unit: currency/ft (imperial) or currency/m (metric).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/valleys",
                  "units": "imperial",
                  "input": {
                    "valleyCount": 2,
                    "valleyLength": 20,
                    "pitch": 6,
                    "overlapAllowance": 0,
                    "wastePercent": 0
                  },
                  "result": {
                    "rawLength": 44.7214,
                    "overlap": 0,
                    "totalLength": 44.7214,
                    "pieces10ft": 5,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "rawLength": "ft",
                    "overlap": "ft",
                    "totalLength": "ft",
                    "pieces10ft": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/ventilation": {
      "get": {
        "operationId": "roofingVentilation",
        "tags": [
          "Roofing"
        ],
        "summary": "Attic ventilation (NFA, intake & exhaust vents)",
        "description": "Net free area (NFA) at the 1:150 (or 1:300) rule: attic area × 144 ÷ ratio, split between intake and exhaust, and the vent counts for your products' NFA ratings.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "atticArea",
            "in": "query",
            "required": true,
            "description": "Attic floor area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 1500
          },
          {
            "name": "exhaustVentNFA",
            "in": "query",
            "required": false,
            "description": "NFA of one exhaust vent (sq in). Unit: sq in (imperial) or cm² (metric). Default 50 sq in (≈322.58 cm²).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 50
            }
          },
          {
            "name": "intakeVentNFA",
            "in": "query",
            "required": false,
            "description": "NFA of one intake/soffit vent (sq in). Unit: sq in (imperial) or cm² (metric). Default 9 sq in (≈58.064 cm²).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 9
            }
          },
          {
            "name": "ratio",
            "in": "query",
            "required": false,
            "description": "Code ratio: 1:150, or 1:300 with a vapor retarder / balanced system.",
            "schema": {
              "type": "string",
              "enum": [
                "150",
                "300"
              ],
              "default": "150"
            }
          },
          {
            "name": "intakePercent",
            "in": "query",
            "required": false,
            "description": "Share of NFA placed at intake. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "costPerVent",
            "in": "query",
            "required": false,
            "description": "Price per vent (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/ventilation",
                  "units": "imperial",
                  "input": {
                    "atticArea": 1500,
                    "exhaustVentNFA": 50,
                    "intakeVentNFA": 9,
                    "ratio": "150",
                    "intakePercent": 50
                  },
                  "result": {
                    "totalNFA": 1440,
                    "intakeNFA": 720,
                    "exhaustNFA": 720,
                    "exhaustVents": 15,
                    "intakeVents": 80,
                    "balanced": true,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "totalNFA": "sq in",
                    "intakeNFA": "sq in",
                    "exhaustNFA": "sq in",
                    "exhaustVents": "count",
                    "intakeVents": "count"
                  },
                  "notes": [
                    "Good balance — aim for a 50/50 intake-to-exhaust split."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/waste-factor": {
      "get": {
        "operationId": "roofingWasteFactor",
        "tags": [
          "Roofing"
        ],
        "summary": "Waste factor with complexity add-ons",
        "description": "Builds a waste factor from a base % plus automatic adders (skylights +2, dormers +3, multiple valleys +3, steep pitch +2), then returns total area, squares and bundles (3 per square).\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "roofArea",
            "in": "query",
            "required": true,
            "description": "True roof area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 2236
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Base waste. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "extraPercent",
            "in": "query",
            "required": false,
            "description": "Any additional waste. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 0
            }
          },
          {
            "name": "hasSkylights",
            "in": "query",
            "required": false,
            "description": "Roof has skylights (+2%).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "hasDormers",
            "in": "query",
            "required": false,
            "description": "Roof has dormers (+3%).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "hasMultipleValleys",
            "in": "query",
            "required": false,
            "description": "Roof has multiple valleys (+3%).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "steepPitch",
            "in": "query",
            "required": false,
            "description": "Steep pitch, 8/12+ (+2%).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "costPerSquare",
            "in": "query",
            "required": false,
            "description": "Installed cost per square (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/waste-factor",
                  "units": "imperial",
                  "input": {
                    "roofArea": 2236,
                    "wastePercent": 10,
                    "extraPercent": 0,
                    "hasSkylights": false,
                    "hasDormers": false,
                    "hasMultipleValleys": false,
                    "steepPitch": false
                  },
                  "result": {
                    "autoAddedPercent": 0,
                    "totalWastePercent": 10,
                    "wasteArea": 223.6,
                    "totalArea": 2459.6,
                    "squares": 24.596,
                    "bundles": 74,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "autoAddedPercent": "%",
                    "totalWastePercent": "%",
                    "wasteArea": "sq ft",
                    "totalArea": "sq ft",
                    "bundles": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/gutters": {
      "get": {
        "operationId": "roofingGutters",
        "tags": [
          "Roofing"
        ],
        "summary": "Gutters, downspouts, hangers, corners",
        "description": "Gutter takeoff: run length with waste, downspouts (one per spacing, minimum 2) at 10 ft per story, hangers at the given spacing, a corner per 40 ft and two end caps per downspout.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Total gutter run (eaves). Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 140
          },
          {
            "name": "stories",
            "in": "query",
            "required": false,
            "description": "Stories (sets downspout length).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5,
              "default": 1
            }
          },
          {
            "name": "downspoutSpacing",
            "in": "query",
            "required": false,
            "description": "Max run per downspout. Unit: ft (imperial) or m (metric). Must be greater than 0. Default 35 ft (≈10.668 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 35
            }
          },
          {
            "name": "hangerSpacing",
            "in": "query",
            "required": false,
            "description": "Hanger spacing. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 48 in (1219.2 mm). Default 24 in (≈609.6 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 24
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 5
            }
          },
          {
            "name": "costPerFoot",
            "in": "query",
            "required": false,
            "description": "Installed price per foot (optional). Unit: currency/ft (imperial) or currency/m (metric).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/gutters",
                  "units": "imperial",
                  "input": {
                    "length": 140,
                    "stories": 1,
                    "downspoutSpacing": 35,
                    "hangerSpacing": 24,
                    "wastePercent": 5
                  },
                  "result": {
                    "gutterLength": 147,
                    "downspouts": 4,
                    "downspoutLength": 40,
                    "hangers": 74,
                    "corners": 4,
                    "endCaps": 8,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "gutterLength": "ft",
                    "downspouts": "count",
                    "downspoutLength": "ft",
                    "hangers": "count",
                    "corners": "count",
                    "endCaps": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/trusses": {
      "get": {
        "operationId": "roofingTrusses",
        "tags": [
          "Roofing"
        ],
        "summary": "Truss count, rafter length, ridge height",
        "description": "Common trusses = ceil(length × 12 ÷ spacing) + 1, optional 2 gable-end trusses; rafter length from half-span and rise plus overhang.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "span",
            "in": "query",
            "required": true,
            "description": "Building width (span). Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 30
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Building length along the ridge. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 40
          },
          {
            "name": "pitch",
            "in": "query",
            "required": false,
            "description": "Rise per 12 of run.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 24,
              "default": 6
            }
          },
          {
            "name": "spacing",
            "in": "query",
            "required": false,
            "description": "Truss spacing on center. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 48 in (1219.2 mm). Default 24 in (≈609.6 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 24
            }
          },
          {
            "name": "overhang",
            "in": "query",
            "required": false,
            "description": "Eave overhang. Unit: in (imperial) or mm (metric). Max 48 in (1219.2 mm). Default 12 in (≈304.8 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 12
            }
          },
          {
            "name": "includeGables",
            "in": "query",
            "required": false,
            "description": "Add two gable-end trusses.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/trusses",
                  "units": "imperial",
                  "input": {
                    "span": 30,
                    "length": 40,
                    "pitch": 6,
                    "spacing": 24,
                    "overhang": 12,
                    "includeGables": false
                  },
                  "result": {
                    "rafterLength": 17.7705,
                    "ridgeHeight": 7.5,
                    "commonTrusses": 21,
                    "gableTrusses": 0,
                    "totalTrusses": 21
                  },
                  "units_of_measure": {
                    "rafterLength": "ft",
                    "ridgeHeight": "ft",
                    "commonTrusses": "count",
                    "gableTrusses": "count",
                    "totalTrusses": "count"
                  },
                  "notes": [
                    "Verify truss design with an engineer for your load zone."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/metal-cost": {
      "get": {
        "operationId": "roofingMetalCost",
        "tags": [
          "Roofing"
        ],
        "summary": "Metal roof installed cost estimate",
        "description": "Planning estimate for a metal roof: panel material (type × substrate × gauge, with waste), trim at $4.50/LF, fasteners, underlayment and regional labor adjusted for pitch, plus contingency.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "True roof area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 2000
          },
          {
            "name": "panelType",
            "in": "query",
            "required": false,
            "description": "Panel profile.",
            "schema": {
              "type": "string",
              "enum": [
                "standing-seam",
                "ribbed",
                "corrugated"
              ],
              "default": "standing-seam"
            }
          },
          {
            "name": "substrate",
            "in": "query",
            "required": false,
            "description": "Metal.",
            "schema": {
              "type": "string",
              "enum": [
                "galvalume",
                "galvanized",
                "aluminum",
                "copper"
              ],
              "default": "galvalume"
            }
          },
          {
            "name": "gauge",
            "in": "query",
            "required": false,
            "description": "Gauge.",
            "schema": {
              "type": "string",
              "enum": [
                "29",
                "26",
                "24",
                "22"
              ],
              "default": "26"
            }
          },
          {
            "name": "fasteners",
            "in": "query",
            "required": false,
            "description": "Fastener system.",
            "schema": {
              "type": "string",
              "enum": [
                "exposed",
                "concealed"
              ],
              "default": "concealed"
            }
          },
          {
            "name": "region",
            "in": "query",
            "required": false,
            "description": "Labor cost region.",
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "average",
                "high",
                "premium"
              ],
              "default": "average"
            }
          },
          {
            "name": "steepness",
            "in": "query",
            "required": false,
            "description": "Pitch band (labor multiplier 1.0 / 1.1 / 1.3).",
            "schema": {
              "type": "string",
              "enum": [
                "low",
                "medium",
                "steep"
              ],
              "default": "medium"
            }
          },
          {
            "name": "trimLength",
            "in": "query",
            "required": false,
            "description": "Total trim (ridge, valley, eave, rake, wall, chimney). Unit: ft (imperial) or m (metric). Default 0 ft (≈0 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Panel waste. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 8
            }
          },
          {
            "name": "contingencyPercent",
            "in": "query",
            "required": false,
            "description": "Contingency. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/metal-cost",
                  "units": "imperial",
                  "input": {
                    "area": 2000,
                    "panelType": "standing-seam",
                    "substrate": "galvalume",
                    "gauge": "26",
                    "fasteners": "concealed",
                    "region": "average",
                    "steepness": "medium",
                    "trimLength": 0,
                    "wastePercent": 8,
                    "contingencyPercent": 5
                  },
                  "result": {
                    "panels": 9720,
                    "trim": 0,
                    "fasteners": 700,
                    "underlayment": 900,
                    "labor": 8250,
                    "subtotal": 19570,
                    "contingency": 978.5,
                    "total": 20548.5,
                    "costPerArea": 10.2743
                  },
                  "units_of_measure": {
                    "panels": "currency",
                    "trim": "currency",
                    "fasteners": "currency",
                    "underlayment": "currency",
                    "labor": "currency",
                    "subtotal": "currency",
                    "contingency": "currency",
                    "total": "currency",
                    "costPerArea": "currency/sq ft"
                  },
                  "notes": [
                    "Planning figure only — get three contractor quotes."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/flat-roof": {
      "get": {
        "operationId": "roofingFlatRoof",
        "tags": [
          "Roofing"
        ],
        "summary": "Flat roof membrane rolls & adhesive",
        "description": "Membrane rolls (TPO/EPDM/PVC 500 sq ft, BUR 100 sq ft), adhesive at 1 gal per 100 sq ft, and edge flashing length.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Roof length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 40
          },
          {
            "name": "width",
            "in": "query",
            "required": true,
            "description": "Roof width. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 25
          },
          {
            "name": "membrane",
            "in": "query",
            "required": false,
            "description": "Membrane type.",
            "schema": {
              "type": "string",
              "enum": [
                "tpo",
                "epdm",
                "pvc",
                "bur"
              ],
              "default": "epdm"
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste and seam overlap. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "costPerSqFt",
            "in": "query",
            "required": false,
            "description": "Installed price per area (optional). Unit: currency/sq ft (imperial) or currency/m² (metric).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/flat-roof",
                  "units": "imperial",
                  "input": {
                    "length": 40,
                    "width": 25,
                    "membrane": "epdm",
                    "wastePercent": 10
                  },
                  "result": {
                    "area": 1000,
                    "areaWithWaste": 1100,
                    "rolls": 3,
                    "adhesive": 11,
                    "edgeFlashing": 130,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "area": "sq ft",
                    "areaWithWaste": "sq ft",
                    "rolls": "count",
                    "adhesive": "US gal",
                    "edgeFlashing": "ft"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/tile-roof": {
      "get": {
        "operationId": "roofingTileRoof",
        "tags": [
          "Roofing"
        ],
        "summary": "Concrete / clay / slate roof tiles and weight",
        "description": "Tiles per square by type (flat concrete 90, S concrete 75, barrel clay 65, flat clay 85, slate 80), battens and total dead load.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Footprint area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 2000
          },
          {
            "name": "tileType",
            "in": "query",
            "required": false,
            "description": "Tile type.",
            "schema": {
              "type": "string",
              "enum": [
                "concrete-flat",
                "concrete-s",
                "clay-barrel",
                "clay-flat",
                "slate"
              ],
              "default": "concrete-flat"
            }
          },
          {
            "name": "pitch",
            "in": "query",
            "required": false,
            "description": "Roof pitch as rise in inches per 12 in of run (6 = a 6/12 roof). Ignored when pitchFactor is given.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 36,
              "default": 6
            },
            "example": 6
          },
          {
            "name": "pitchFactor",
            "in": "query",
            "required": false,
            "description": "Slope multiplier to apply directly (e.g. 1.118 for 6/12). Overrides pitch.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 4
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "costPerTile",
            "in": "query",
            "required": false,
            "description": "Price per tile (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/tile-roof",
                  "units": "imperial",
                  "input": {
                    "area": 2000,
                    "tileType": "concrete-flat",
                    "pitch": 6,
                    "wastePercent": 10
                  },
                  "result": {
                    "roofArea": 2236.068,
                    "areaWithWaste": 2459.6748,
                    "squares": 24.5967,
                    "tiles": 2214,
                    "battens": 2459.6748,
                    "totalWeight": 22140,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "roofArea": "sq ft",
                    "areaWithWaste": "sq ft",
                    "tiles": "count",
                    "battens": "ft",
                    "totalWeight": "lb"
                  },
                  "notes": [
                    "Heavy load — structural review strongly recommended."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/coating": {
      "get": {
        "operationId": "roofingCoating",
        "tags": [
          "Roofing"
        ],
        "summary": "Roof coating gallons and 5-gal pails",
        "description": "Elastomeric/silicone coating: area × coats × (1 + waste) ÷ coverage per gallon, in 5-gallon pails.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Roof area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 1500
          },
          {
            "name": "coats",
            "in": "query",
            "required": false,
            "description": "Number of coats.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5,
              "default": 2
            }
          },
          {
            "name": "coveragePerGallon",
            "in": "query",
            "required": false,
            "description": "Coverage per gallon per coat. Unit: sq ft/gal (imperial) or m²/L (metric). Must be greater than 0. Default 100 sq ft/gal (≈2.454 m²/L).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 100
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "costPerGallon",
            "in": "query",
            "required": false,
            "description": "Price per gallon (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/coating",
                  "units": "imperial",
                  "input": {
                    "area": 1500,
                    "coats": 2,
                    "coveragePerGallon": 100,
                    "wastePercent": 10
                  },
                  "result": {
                    "coverageArea": 3300,
                    "gallonsExact": 33,
                    "gallonCans": 34,
                    "pails5gal": 7,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "coverageArea": "sq ft",
                    "gallonsExact": "US gal",
                    "gallonCans": "count",
                    "pails5gal": "count"
                  },
                  "notes": [
                    "Two coats give the best waterproofing."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/snow-load": {
      "get": {
        "operationId": "roofingSnowLoad",
        "tags": [
          "Roofing"
        ],
        "summary": "Snow load on a roof vs its capacity",
        "description": "Snow load in psf from depth × weight per inch (fresh 1.25, packed 3, wet 5, ice 5.7 psf/in), reduced for pitch, compared with the roof's rated capacity.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Roof area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 1500
          },
          {
            "name": "snowDepth",
            "in": "query",
            "required": true,
            "description": "Snow depth. Unit: in (imperial) or mm (metric). Max 240 in (6096 mm).",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 18
          },
          {
            "name": "snowType",
            "in": "query",
            "required": false,
            "description": "Snow condition.",
            "schema": {
              "type": "string",
              "enum": [
                "fresh",
                "packed",
                "wet",
                "ice"
              ],
              "default": "packed"
            }
          },
          {
            "name": "pitch",
            "in": "query",
            "required": false,
            "description": "Rise per 12 of run.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 24,
              "default": 4
            }
          },
          {
            "name": "capacity",
            "in": "query",
            "required": false,
            "description": "Rated roof snow capacity. Unit: lb/sq ft (imperial) or kg/m² (metric). Must be greater than 0. Default 30 lb/sq ft (≈146.473 kg/m²).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 30
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/snow-load",
                  "units": "imperial",
                  "input": {
                    "area": 1500,
                    "snowDepth": 18,
                    "snowType": "packed",
                    "pitch": 4,
                    "capacity": 30
                  },
                  "result": {
                    "load": 48.6,
                    "totalWeight": 72900,
                    "totalTons": 36.45,
                    "percentOfCapacity": 162,
                    "status": "critical"
                  },
                  "units_of_measure": {
                    "load": "lb/sq ft",
                    "totalWeight": "lb",
                    "totalTons": "US ton",
                    "percentOfCapacity": "%"
                  },
                  "notes": [
                    "Estimate only. Follow local code and an engineer for structural decisions."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/labor": {
      "get": {
        "operationId": "roofingLabor",
        "tags": [
          "Roofing"
        ],
        "summary": "Roofing crew days and labor hours",
        "description": "Install days = squares ÷ squares per crew-day × complexity; tear-off adds 50%; hours = days × 8 × crew size.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "squares",
            "in": "query",
            "required": true,
            "description": "Roof squares. Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 25
          },
          {
            "name": "crewSize",
            "in": "query",
            "required": false,
            "description": "Crew size.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 30,
              "default": 4
            }
          },
          {
            "name": "squaresPerDay",
            "in": "query",
            "required": false,
            "description": "Squares a crew installs per day. Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 10
            }
          },
          {
            "name": "tearOff",
            "in": "query",
            "required": false,
            "description": "Include tear-off of the old roof.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "complexity",
            "in": "query",
            "required": false,
            "description": "Complexity multiplier (1 simple – 1.5 complex).",
            "schema": {
              "type": "number",
              "minimum": 0.5,
              "maximum": 3,
              "default": 1
            }
          },
          {
            "name": "laborRate",
            "in": "query",
            "required": false,
            "description": "Hourly rate per worker (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/labor",
                  "units": "imperial",
                  "input": {
                    "squares": 25,
                    "crewSize": 4,
                    "squaresPerDay": 10,
                    "tearOff": true,
                    "complexity": 1
                  },
                  "result": {
                    "installDays": 2.5,
                    "tearOffDays": 1.25,
                    "totalDays": 3.75,
                    "laborHours": 120,
                    "laborCost": null
                  },
                  "units_of_measure": {
                    "installDays": "days",
                    "tearOffDays": "days",
                    "totalDays": "days",
                    "laborHours": "h"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/roofing/solar-potential": {
      "get": {
        "operationId": "roofingSolarPotential",
        "tags": [
          "Roofing"
        ],
        "summary": "Rooftop solar panels, kW, output and payback",
        "description": "Panels that fit the usable roof (17.5 sq ft each), system kW, annual kWh and savings, and payback after a 30% tax credit.\n\nSource: smartroofingcalculator.com (src/lib/calculations.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Roof area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 1200
          },
          {
            "name": "usablePercent",
            "in": "query",
            "required": false,
            "description": "Share of the roof that is usable (orientation, obstructions). Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 60
            }
          },
          {
            "name": "panelWatts",
            "in": "query",
            "required": false,
            "description": "Panel rating (W). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1000,
              "default": 400
            }
          },
          {
            "name": "sunHours",
            "in": "query",
            "required": false,
            "description": "Peak sun hours per day. Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 12,
              "default": 4.5
            }
          },
          {
            "name": "electricRate",
            "in": "query",
            "required": false,
            "description": "Electricity price per kWh. Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0.16
            }
          },
          {
            "name": "costPerWatt",
            "in": "query",
            "required": false,
            "description": "Installed price per watt (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/roofing/solar-potential",
                  "units": "imperial",
                  "input": {
                    "area": 1200,
                    "usablePercent": 60,
                    "panelWatts": 400,
                    "sunHours": 4.5,
                    "electricRate": 0.16
                  },
                  "result": {
                    "usableArea": 720,
                    "panels": 41,
                    "systemSize": 16.4,
                    "dailyOutput": 73.8,
                    "annualOutput": 26937,
                    "annualSavings": 4309.92,
                    "systemCost": null,
                    "costAfterCredit": null,
                    "paybackYears": null
                  },
                  "units_of_measure": {
                    "usableArea": "sq ft",
                    "panels": "count",
                    "systemSize": "kW",
                    "dailyOutput": "kWh",
                    "annualOutput": "kWh",
                    "annualSavings": "currency"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/decking/cost": {
      "get": {
        "operationId": "deckingCost",
        "tags": [
          "Decking"
        ],
        "summary": "Deck cost estimate (low–high) with breakdown",
        "description": "The published deck cost estimator: installed $/sq ft band by material, split 60/40 materials/labor, height multiplier, regional labor by US ZIP first digit, railing on the perimeter, stairs, add-ons and permits. DIY halves labor and adds tool rental.\n\nSource: smartdeckingcalculator.com (src/lib/calculator.ts). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Deck length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 16
          },
          {
            "name": "width",
            "in": "query",
            "required": true,
            "description": "Deck width. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 12
          },
          {
            "name": "shape",
            "in": "query",
            "required": false,
            "description": "Deck shape (L-shape uses 78% of the bounding box).",
            "schema": {
              "type": "string",
              "enum": [
                "rectangle",
                "lshape"
              ],
              "default": "rectangle"
            }
          },
          {
            "name": "material",
            "in": "query",
            "required": false,
            "description": "Decking material.",
            "schema": {
              "type": "string",
              "enum": [
                "pt",
                "cedar",
                "redwood",
                "comp_entry",
                "comp_premium",
                "pvc",
                "hardwood"
              ],
              "default": "comp_entry"
            }
          },
          {
            "name": "height",
            "in": "query",
            "required": false,
            "description": "Height band: ground, low 2–4 ft, mid 4–8 ft, high 8 ft+.",
            "schema": {
              "type": "string",
              "enum": [
                "ground",
                "low",
                "mid",
                "high"
              ],
              "default": "low"
            }
          },
          {
            "name": "railing",
            "in": "query",
            "required": false,
            "description": "Railing type.",
            "schema": {
              "type": "string",
              "enum": [
                "none",
                "wood",
                "composite",
                "aluminum",
                "cable"
              ],
              "default": "composite"
            }
          },
          {
            "name": "stairs",
            "in": "query",
            "required": false,
            "description": "Number of stair steps.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100,
              "default": 4
            }
          },
          {
            "name": "addons",
            "in": "query",
            "required": false,
            "description": "Comma-separated add-ons. Comma-separated; any of: benches, pergola, lighting, skirting, hottub, multilevel.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "zip",
            "in": "query",
            "required": false,
            "description": "US ZIP code; its first digit sets the regional labor multiplier (optional).",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{5}$",
              "maxLength": 64
            },
            "example": "10001"
          },
          {
            "name": "diy",
            "in": "query",
            "required": false,
            "description": "Build it yourself (halves labor, adds tool rental).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/decking/cost",
                  "units": "imperial",
                  "input": {
                    "length": 16,
                    "width": 12,
                    "shape": "rectangle",
                    "material": "comp_entry",
                    "height": "low",
                    "railing": "composite",
                    "stairs": 4,
                    "addons": [],
                    "zip": "10001",
                    "diy": false
                  },
                  "result": {
                    "area": 192,
                    "totalLow": 9174,
                    "totalHigh": 13720,
                    "perAreaLow": 48,
                    "perAreaHigh": 71,
                    "regionMultiplier": 1.15,
                    "breakdown": {
                      "materials": {
                        "low": 6374,
                        "high": 9246
                      },
                      "labor": {
                        "low": 2650,
                        "high": 3974
                      },
                      "permits": {
                        "low": 150,
                        "high": 500
                      },
                      "addons": {
                        "low": 0,
                        "high": 0
                      }
                    }
                  },
                  "units_of_measure": {
                    "area": "sq ft",
                    "totalLow": "currency",
                    "totalHigh": "currency",
                    "perAreaLow": "currency/sq ft",
                    "perAreaHigh": "currency/sq ft",
                    "breakdown.materials.low": "currency",
                    "breakdown.materials.high": "currency",
                    "breakdown.labor.low": "currency",
                    "breakdown.labor.high": "currency",
                    "breakdown.permits.low": "currency",
                    "breakdown.permits.high": "currency",
                    "breakdown.addons.low": "currency",
                    "breakdown.addons.high": "currency"
                  },
                  "notes": [
                    "US 2026 planning ranges. Get at least three local quotes."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/decking/boards": {
      "get": {
        "operationId": "deckingBoards",
        "tags": [
          "Decking"
        ],
        "summary": "Deck boards (rows × boards per row + pattern waste)",
        "description": "Boards run along the length; rows stack across the width at board face + gap. The waste you give is floored at the pattern minimum (straight 5%, diagonal 12%, picture-frame 15%, herringbone 18%).\n\nSource: smartdeckingcalculator.com + DeckFenceMath.kt (studio deck app port). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Deck length (board direction). Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 16
          },
          {
            "name": "width",
            "in": "query",
            "required": true,
            "description": "Deck width (across the boards). Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 12
          },
          {
            "name": "boardWidth",
            "in": "query",
            "required": false,
            "description": "Actual board face width (5.5 for a 5/4×6 or 2×6). Unit: in (imperial) or mm (metric). Must be greater than 0. Max 12 in (304.8 mm). Default 5.5 in (≈139.7 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 5.5
            }
          },
          {
            "name": "boardLength",
            "in": "query",
            "required": false,
            "description": "Board length. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 24 ft (7.315 m). Default 12 ft (≈3.658 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 12
            }
          },
          {
            "name": "gap",
            "in": "query",
            "required": false,
            "description": "Gap between boards. Unit: in (imperial) or mm (metric). Max 1 in (25.4 mm). Default 0.125 in (≈3.175 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0.125
            }
          },
          {
            "name": "pattern",
            "in": "query",
            "required": false,
            "description": "Laying pattern.",
            "schema": {
              "type": "string",
              "enum": [
                "straight",
                "diagonal",
                "picture-frame",
                "herringbone"
              ],
              "default": "straight"
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste (raised to the pattern minimum if lower). Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/decking/boards",
                  "units": "imperial",
                  "input": {
                    "length": 16,
                    "width": 12,
                    "boardWidth": 5.5,
                    "boardLength": 12,
                    "gap": 0.125,
                    "pattern": "straight",
                    "wastePercent": 0
                  },
                  "result": {
                    "area": 192,
                    "rows": 26,
                    "boardsPerRow": 2,
                    "baseBoards": 52,
                    "wasteBoards": 3,
                    "effectiveWastePercent": 5,
                    "totalBoards": 55,
                    "totalLength": 660
                  },
                  "units_of_measure": {
                    "area": "sq ft",
                    "rows": "count",
                    "boardsPerRow": "count",
                    "baseBoards": "count",
                    "wasteBoards": "count",
                    "effectiveWastePercent": "%",
                    "totalBoards": "count",
                    "totalLength": "ft"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/decking/joists": {
      "get": {
        "operationId": "deckingJoists",
        "tags": [
          "Decking"
        ],
        "summary": "Joist count and linear feet",
        "description": "Joists = ceil(run × 12 ÷ spacing) + 1 (the end joist). Linear feet = joists × joist span, plus rim joists.\n\nSource: smartdeckingcalculator.com + DeckFenceMath.kt (studio deck app port). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Run the joists are spaced along (usually the deck length). Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 16
          },
          {
            "name": "joistSpan",
            "in": "query",
            "required": true,
            "description": "Length of each joist (deck depth). Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 12
          },
          {
            "name": "spacing",
            "in": "query",
            "required": false,
            "description": "Joist spacing on center (16 standard, 12 for diagonal/composite). Unit: in (imperial) or mm (metric). Must be greater than 0. Max 24 in (609.6 mm). Default 16 in (≈406.4 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 16
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/decking/joists",
                  "units": "imperial",
                  "input": {
                    "length": 16,
                    "joistSpan": 12,
                    "spacing": 16
                  },
                  "result": {
                    "joists": 13,
                    "joistLength": 156,
                    "rimJoistLength": 32,
                    "hangers": 26
                  },
                  "units_of_measure": {
                    "joists": "count",
                    "joistLength": "ft",
                    "rimJoistLength": "ft",
                    "hangers": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/decking/screws": {
      "get": {
        "operationId": "deckingScrews",
        "tags": [
          "Decking"
        ],
        "summary": "Deck screws per sq ft, total, pounds and boxes",
        "description": "Two screws at every joist crossing × 1.5 for ends and butt joints; ~75 #10×2½\" screws per lb; 1 lb and 5 lb boxes.\n\nSource: smartdeckingcalculator.com + DeckFenceMath.kt (studio deck app port). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Deck area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 192
          },
          {
            "name": "boardWidth",
            "in": "query",
            "required": false,
            "description": "Board face width. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 12 in (304.8 mm). Default 5.5 in (≈139.7 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 5.5
            }
          },
          {
            "name": "joistSpacing",
            "in": "query",
            "required": false,
            "description": "Joist spacing. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 24 in (609.6 mm). Default 16 in (≈406.4 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 16
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/decking/screws",
                  "units": "imperial",
                  "input": {
                    "area": 192,
                    "boardWidth": 5.5,
                    "joistSpacing": 16
                  },
                  "result": {
                    "screwsPerSqFt": 4.9,
                    "screws": 941,
                    "weight": 12.5,
                    "boxes1lb": 13,
                    "boxes5lb": 3
                  },
                  "units_of_measure": {
                    "screws": "count",
                    "weight": "lb",
                    "boxes1lb": "count",
                    "boxes5lb": "count"
                  },
                  "notes": [
                    "Composite and PVC boards on hidden clips use one clip per crossing instead."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/decking/posts": {
      "get": {
        "operationId": "deckingPosts",
        "tags": [
          "Decking"
        ],
        "summary": "Deck posts per beam and total",
        "description": "Posts along each beam: one per max spacing of run, plus the end post; returns the actual spacing.\n\nSource: smartdeckingcalculator.com + DeckFenceMath.kt (studio deck app port). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "beamLength",
            "in": "query",
            "required": true,
            "description": "Beam length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 16
          },
          {
            "name": "maxSpacing",
            "in": "query",
            "required": false,
            "description": "Maximum post spacing (per span tables, often 6–8 ft). Unit: ft (imperial) or m (metric). Must be greater than 0. Max 16 ft (4.877 m). Default 8 ft (≈2.438 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 8
            }
          },
          {
            "name": "beams",
            "in": "query",
            "required": false,
            "description": "Number of beams (1 ledger-attached, 2 freestanding).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/decking/posts",
                  "units": "imperial",
                  "input": {
                    "beamLength": 16,
                    "maxSpacing": 8,
                    "beams": 1
                  },
                  "result": {
                    "postsPerBeam": 3,
                    "beams": 1,
                    "totalPosts": 3,
                    "actualSpacing": 8
                  },
                  "units_of_measure": {
                    "postsPerBeam": "count",
                    "beams": "count",
                    "totalPosts": "count",
                    "actualSpacing": "ft"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/decking/footings": {
      "get": {
        "operationId": "deckingFootings",
        "tags": [
          "Decking"
        ],
        "summary": "Footing diameter and concrete bags per post",
        "description": "Post load = tributary area (post spacing × deck depth ÷ 2) × design load (40 live + 10 dead psf); footing area = load ÷ soil bearing (IRC presumptive 1,500 psf); recommends a stock tube size and counts concrete bags for the holes.\n\nSource: smartdeckingcalculator.com + DeckFenceMath.kt (studio deck app port). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "postSpacing",
            "in": "query",
            "required": true,
            "description": "Post spacing along the beam. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 8
          },
          {
            "name": "deckDepth",
            "in": "query",
            "required": true,
            "description": "Joist span (deck depth). Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 12
          },
          {
            "name": "posts",
            "in": "query",
            "required": false,
            "description": "Number of footings to pour.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 1
            }
          },
          {
            "name": "soilBearing",
            "in": "query",
            "required": false,
            "description": "Soil bearing capacity. Unit: lb/sq ft (imperial) or kg/m² (metric). Must be greater than 0. Max 12000 lb/sq ft (58589.132 kg/m²). Default 1500 lb/sq ft (≈7323.641 kg/m²).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 1500
            }
          },
          {
            "name": "designLoad",
            "in": "query",
            "required": false,
            "description": "Design load (live + dead). Unit: lb/sq ft (imperial) or kg/m² (metric). Must be greater than 0. Max 200 lb/sq ft (976.486 kg/m²). Default 50 lb/sq ft (≈244.121 kg/m²).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 50
            }
          },
          {
            "name": "holeDepth",
            "in": "query",
            "required": false,
            "description": "Footing depth (below frost line). Unit: in (imperial) or mm (metric). Must be greater than 0. Max 120 in (3048 mm). Default 36 in (≈914.4 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 36
            }
          },
          {
            "name": "bagSize",
            "in": "query",
            "required": false,
            "description": "Concrete bag size (lb).",
            "schema": {
              "type": "string",
              "enum": [
                "40",
                "60",
                "80"
              ],
              "default": "80"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/decking/footings",
                  "units": "imperial",
                  "input": {
                    "postSpacing": 8,
                    "deckDepth": 12,
                    "posts": 1,
                    "soilBearing": 1500,
                    "designLoad": 50,
                    "holeDepth": 36,
                    "bagSize": "80"
                  },
                  "result": {
                    "tributaryArea": 48,
                    "postLoad": 2400,
                    "minDiameter": 17.1276,
                    "recommendedDiameter": 20,
                    "concrete": 6.545,
                    "bags": 11
                  },
                  "units_of_measure": {
                    "tributaryArea": "sq ft",
                    "postLoad": "lb",
                    "minDiameter": "in",
                    "recommendedDiameter": "in",
                    "concrete": "cu ft",
                    "bags": "count"
                  },
                  "notes": [
                    "Confirm footing size and depth with your local building department."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/decking/balusters": {
      "get": {
        "operationId": "deckingBalusters",
        "tags": [
          "Decking"
        ],
        "summary": "Railing balusters under the 4-inch rule",
        "description": "Posts every section; balusters fill each run so no gap reaches 4\" (IRC R312.1.3): n = ceil((usable − 4) ÷ (baluster + 4)); returns the real gap.\n\nSource: smartdeckingcalculator.com + DeckFenceMath.kt (studio deck app port). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "railingLength",
            "in": "query",
            "required": true,
            "description": "Total railing run. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 16
          },
          {
            "name": "postSpacing",
            "in": "query",
            "required": false,
            "description": "Post spacing. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 10 ft (3.048 m). Default 6 ft (≈1.829 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 6
            }
          },
          {
            "name": "postWidth",
            "in": "query",
            "required": false,
            "description": "Post width. Unit: in (imperial) or mm (metric). Max 8 in (203.2 mm). Default 3.5 in (≈88.9 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 3.5
            }
          },
          {
            "name": "balusterWidth",
            "in": "query",
            "required": false,
            "description": "Baluster width. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 6 in (152.4 mm). Default 1.5 in (≈38.1 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 1.5
            }
          },
          {
            "name": "maxGap",
            "in": "query",
            "required": false,
            "description": "Maximum gap. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 6 in (152.4 mm). Default 4 in (≈101.6 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 4
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/decking/balusters",
                  "units": "imperial",
                  "input": {
                    "railingLength": 16,
                    "postSpacing": 6,
                    "postWidth": 3.5,
                    "balusterWidth": 1.5,
                    "maxGap": 4
                  },
                  "result": {
                    "sections": 3,
                    "posts": 4,
                    "balusters": 32,
                    "actualGap": 3.9394,
                    "railLength": 32
                  },
                  "units_of_measure": {
                    "sections": "count",
                    "posts": "count",
                    "balusters": "count",
                    "actualGap": "in",
                    "railLength": "ft"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/decking/cable-railing": {
      "get": {
        "operationId": "deckingCableRailing",
        "tags": [
          "Decking"
        ],
        "summary": "Cable railing runs, cable length, posts, tensioners",
        "description": "Cable runs at ≤3\" spacing inside a 36\" rail, posts ≤4 ft apart, cable = runs × (length + 2 ft) × 1.05, one tensioner pair per run per ≤30 ft.\n\nSource: smartdeckingcalculator.com + DeckFenceMath.kt (studio deck app port). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "railingLength",
            "in": "query",
            "required": true,
            "description": "Railing length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 40
          },
          {
            "name": "railHeight",
            "in": "query",
            "required": false,
            "description": "Rail height. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 48 in (1219.2 mm). Default 36 in (≈914.4 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 36
            }
          },
          {
            "name": "cableSpacing",
            "in": "query",
            "required": false,
            "description": "Cable spacing. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 4 in (101.6 mm). Default 3 in (≈76.2 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 3
            }
          },
          {
            "name": "postSpacing",
            "in": "query",
            "required": false,
            "description": "Post spacing. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 6 ft (1.829 m). Default 4 ft (≈1.219 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 4
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/decking/cable-railing",
                  "units": "imperial",
                  "input": {
                    "railingLength": 40,
                    "railHeight": 36,
                    "cableSpacing": 3,
                    "postSpacing": 4
                  },
                  "result": {
                    "cableRuns": 10,
                    "cableLength": 441,
                    "posts": 11,
                    "tensioners": 40
                  },
                  "units_of_measure": {
                    "cableRuns": "count",
                    "cableLength": "ft",
                    "posts": "count",
                    "tensioners": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/decking/stain": {
      "get": {
        "operationId": "deckingStain",
        "tags": [
          "Decking"
        ],
        "summary": "Deck stain / sealer gallons",
        "description": "Surface = floor + both faces of the railing (~2 sq ft per LF) + stair treads and risers; gallons = surface × coats ÷ coverage, whole cans.\n\nSource: smartdeckingcalculator.com + DeckFenceMath.kt (studio deck app port). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Deck floor area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 192
          },
          {
            "name": "railingLength",
            "in": "query",
            "required": false,
            "description": "Railing length. Unit: ft (imperial) or m (metric). Default 0 ft (≈0 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0
            },
            "example": 40
          },
          {
            "name": "stairSteps",
            "in": "query",
            "required": false,
            "description": "Stair steps.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100,
              "default": 0
            }
          },
          {
            "name": "coats",
            "in": "query",
            "required": false,
            "description": "Coats.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5,
              "default": 2
            }
          },
          {
            "name": "coveragePerGallon",
            "in": "query",
            "required": false,
            "description": "Coverage per gallon (150–300 typical). Unit: sq ft/gal (imperial) or m²/L (metric). Must be greater than 0. Default 200 sq ft/gal (≈4.908 m²/L).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/decking/stain",
                  "units": "imperial",
                  "input": {
                    "area": 192,
                    "railingLength": 40,
                    "stairSteps": 0,
                    "coats": 2,
                    "coveragePerGallon": 200
                  },
                  "result": {
                    "surface": 272,
                    "gallonCans": 3
                  },
                  "units_of_measure": {
                    "surface": "sq ft",
                    "gallonCans": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/fencing/posts": {
      "get": {
        "operationId": "fencingPosts",
        "tags": [
          "Fencing"
        ],
        "summary": "Fence posts (line + gate)",
        "description": "Line posts = ceil(length ÷ spacing) + 1, plus two per gate.\n\nSource: smartfencingcalculator.com (src/lib/calculations.ts) + DeckFenceMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Fence length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 100
          },
          {
            "name": "postSpacing",
            "in": "query",
            "required": false,
            "description": "Post spacing on center. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 16 ft (4.877 m). Default 8 ft (≈2.438 m).",
            "schema": {
              "type": "number",
              "default": 8
            }
          },
          {
            "name": "gates",
            "in": "query",
            "required": false,
            "description": "Number of gates.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 50,
              "default": 0
            }
          },
          {
            "name": "postCost",
            "in": "query",
            "required": false,
            "description": "Price per post (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/fencing/posts",
                  "units": "imperial",
                  "input": {
                    "length": 100,
                    "postSpacing": 8,
                    "gates": 0
                  },
                  "result": {
                    "linePosts": 14,
                    "gatePosts": 0,
                    "totalPosts": 14,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "linePosts": "count",
                    "gatePosts": "count",
                    "totalPosts": "count"
                  },
                  "notes": [
                    "Use 4×4 line posts and 6×6 corner/gate posts for stiffness."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/fencing/pickets": {
      "get": {
        "operationId": "fencingPickets",
        "tags": [
          "Fencing"
        ],
        "summary": "Fence pickets",
        "description": "Pickets = (length − gate openings) ÷ (picket width + gap) × (1 + waste), rounded up.\n\nSource: smartfencingcalculator.com (src/lib/calculations.ts) + DeckFenceMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Fence length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 100
          },
          {
            "name": "picketWidth",
            "in": "query",
            "required": false,
            "description": "Picket width. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 12 in (304.8 mm). Default 5.5 in (≈139.7 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 5.5
            }
          },
          {
            "name": "gap",
            "in": "query",
            "required": false,
            "description": "Gap between pickets (0 for privacy). Unit: in (imperial) or mm (metric). Max 6 in (152.4 mm). Default 0 in (≈0 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "gates",
            "in": "query",
            "required": false,
            "description": "Number of gates.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 50,
              "default": 0
            }
          },
          {
            "name": "gateWidth",
            "in": "query",
            "required": false,
            "description": "Gate opening width. Unit: ft (imperial) or m (metric). Max 20 ft (6.096 m). Default 4 ft (≈1.219 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 4
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "picketCost",
            "in": "query",
            "required": false,
            "description": "Price per picket (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/fencing/pickets",
                  "units": "imperial",
                  "input": {
                    "length": 100,
                    "picketWidth": 5.5,
                    "gap": 0,
                    "gates": 0,
                    "gateWidth": 4,
                    "wastePercent": 10
                  },
                  "result": {
                    "usableLength": 100,
                    "picketsBeforeWaste": 218.1818,
                    "pickets": 241,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "usableLength": "ft",
                    "pickets": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/fencing/rails": {
      "get": {
        "operationId": "fencingRails",
        "tags": [
          "Fencing"
        ],
        "summary": "Fence rails and rail sticks",
        "description": "Sections = ceil(length ÷ spacing); rails = sections × rails per section; sticks = rail length ÷ stock length.\n\nSource: smartfencingcalculator.com (src/lib/calculations.ts) + DeckFenceMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Fence length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 100
          },
          {
            "name": "postSpacing",
            "in": "query",
            "required": false,
            "description": "Post spacing. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 16 ft (4.877 m). Default 8 ft (≈2.438 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 8
            }
          },
          {
            "name": "railsPerSection",
            "in": "query",
            "required": false,
            "description": "Rails per section (3 for fences over 6 ft).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 6,
              "default": 2
            }
          },
          {
            "name": "stockLength",
            "in": "query",
            "required": false,
            "description": "Rail stock length. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 20 ft (6.096 m). Default 8 ft (≈2.438 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 8
            }
          },
          {
            "name": "railCost",
            "in": "query",
            "required": false,
            "description": "Price per stick (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/fencing/rails",
                  "units": "imperial",
                  "input": {
                    "length": 100,
                    "postSpacing": 8,
                    "railsPerSection": 2,
                    "stockLength": 8
                  },
                  "result": {
                    "sections": 13,
                    "rails": 26,
                    "railLength": 208,
                    "sticks": 26,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "sections": "count",
                    "rails": "count",
                    "railLength": "ft",
                    "sticks": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/fencing/post-concrete": {
      "get": {
        "operationId": "fencingPostConcrete",
        "tags": [
          "Fencing"
        ],
        "summary": "Concrete bags to set fence posts",
        "description": "Per hole: π × (d/2)² × depth − post displacement, in cu ft; bags at the bag yield (80 lb ≈ 0.60 cu ft). Omit holeDepth to use the ⅓-height + 6\" rule (min 24\").\n\nSource: smartfencingcalculator.com (src/lib/calculations.ts) + DeckFenceMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "posts",
            "in": "query",
            "required": true,
            "description": "Number of posts.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5000
            },
            "example": 14
          },
          {
            "name": "holeDiameter",
            "in": "query",
            "required": false,
            "description": "Hole diameter (≈3× post width). Unit: in (imperial) or mm (metric). Must be greater than 0. Max 36 in (914.4 mm). Default 10 in (≈254 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 10
            }
          },
          {
            "name": "holeDepth",
            "in": "query",
            "required": false,
            "description": "Hole depth. Default from fenceHeight. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 120 in (3048 mm).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          },
          {
            "name": "fenceHeight",
            "in": "query",
            "required": false,
            "description": "Above-ground fence height (for the depth rule). Unit: ft (imperial) or m (metric). Must be greater than 0. Max 16 ft (4.877 m). Default 6 ft (≈1.829 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 6
            }
          },
          {
            "name": "postWidth",
            "in": "query",
            "required": false,
            "description": "Post width (3.5 for a 4×4). Unit: in (imperial) or mm (metric). Max 12 in (304.8 mm). Default 3.5 in (≈88.9 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 3.5
            }
          },
          {
            "name": "bagSize",
            "in": "query",
            "required": false,
            "description": "Bag size (lb).",
            "schema": {
              "type": "string",
              "enum": [
                "40",
                "60",
                "80"
              ],
              "default": "80"
            }
          },
          {
            "name": "bagCost",
            "in": "query",
            "required": false,
            "description": "Price per bag (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/fencing/post-concrete",
                  "units": "imperial",
                  "input": {
                    "posts": 14,
                    "holeDiameter": 10,
                    "fenceHeight": 6,
                    "postWidth": 3.5,
                    "bagSize": "80"
                  },
                  "result": {
                    "holeDepth": 30,
                    "holeVolume": 1.3635,
                    "concretePerHole": 1.1509,
                    "totalConcrete": 16.1121,
                    "totalConcreteYards": 0.5967,
                    "bags": 27,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "holeDepth": "in",
                    "holeVolume": "cu ft",
                    "concretePerHole": "cu ft",
                    "totalConcrete": "cu ft",
                    "totalConcreteYards": "cu yd",
                    "bags": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/fencing/panels": {
      "get": {
        "operationId": "fencingPanels",
        "tags": [
          "Fencing"
        ],
        "summary": "Prefab fence panels, posts and brackets",
        "description": "Panels = ceil((length − gate openings) ÷ panel width), the partial last panel, posts (panels + 1 + 2 per gate) and 4 brackets per panel.\n\nSource: smartfencingcalculator.com (src/lib/calculations.ts) + DeckFenceMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Fence length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 100
          },
          {
            "name": "panelWidth",
            "in": "query",
            "required": false,
            "description": "Panel width. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 16 ft (4.877 m). Default 8 ft (≈2.438 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 8
            }
          },
          {
            "name": "gates",
            "in": "query",
            "required": false,
            "description": "Number of gates.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 50,
              "default": 0
            }
          },
          {
            "name": "gateWidth",
            "in": "query",
            "required": false,
            "description": "Gate opening width. Unit: ft (imperial) or m (metric). Max 20 ft (6.096 m). Default 4 ft (≈1.219 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 4
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/fencing/panels",
                  "units": "imperial",
                  "input": {
                    "length": 100,
                    "panelWidth": 8,
                    "gates": 0,
                    "gateWidth": 4
                  },
                  "result": {
                    "panelRun": 100,
                    "panels": 13,
                    "partialPanel": 4,
                    "posts": 14,
                    "brackets": 52
                  },
                  "units_of_measure": {
                    "panelRun": "ft",
                    "panels": "count",
                    "partialPanel": "ft",
                    "posts": "count",
                    "brackets": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/fencing/chain-link": {
      "get": {
        "operationId": "fencingChainLink",
        "tags": [
          "Fencing"
        ],
        "summary": "Chain-link fence full takeoff",
        "description": "Line and terminal posts, 50 ft mesh rolls, 21 ft top rail, tension wire (1,000 ft rolls), ties, tension bands and bars, and one 80 lb bag per post.\n\nSource: smartfencingcalculator.com (src/lib/calculations.ts) + DeckFenceMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Fence length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 150
          },
          {
            "name": "height",
            "in": "query",
            "required": false,
            "description": "Fence height. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 12 ft (3.658 m). Default 4 ft (≈1.219 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 4
            }
          },
          {
            "name": "postSpacing",
            "in": "query",
            "required": false,
            "description": "Line post spacing. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 12 ft (3.658 m). Default 10 ft (≈3.048 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 10
            }
          },
          {
            "name": "gates",
            "in": "query",
            "required": false,
            "description": "Number of gates.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 50,
              "default": 0
            }
          },
          {
            "name": "gateWidth",
            "in": "query",
            "required": false,
            "description": "Gate width. Unit: ft (imperial) or m (metric). Max 20 ft (6.096 m). Default 4 ft (≈1.219 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 4
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/fencing/chain-link",
                  "units": "imperial",
                  "input": {
                    "length": 150,
                    "height": 4,
                    "postSpacing": 10,
                    "gates": 0,
                    "gateWidth": 4
                  },
                  "result": {
                    "netLength": 150,
                    "linePosts": 14,
                    "terminalPosts": 2,
                    "totalPosts": 16,
                    "meshRolls50ft": 3,
                    "topRailSticks21ft": 8,
                    "tensionWireRolls": 1,
                    "ties": 514,
                    "tensionBands": 8,
                    "tensionBars": 2,
                    "concreteBags80lb": 16
                  },
                  "units_of_measure": {
                    "netLength": "ft",
                    "linePosts": "count",
                    "terminalPosts": "count",
                    "totalPosts": "count",
                    "meshRolls50ft": "count",
                    "topRailSticks21ft": "count",
                    "tensionWireRolls": "count",
                    "ties": "count",
                    "tensionBands": "count",
                    "tensionBars": "count",
                    "concreteBags80lb": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/fencing/electric": {
      "get": {
        "operationId": "fencingElectric",
        "tags": [
          "Fencing"
        ],
        "summary": "Electric fence wire, posts, insulators, ground rods, energizer",
        "description": "Wire = perimeter × strands in ¼-mile (1,320 ft) rolls; line posts plus 4 corners; one insulator per strand per post; a ground rod per 300 ft; energizer joules by animal and wire length.\n\nSource: smartfencingcalculator.com (src/lib/calculations.ts) + DeckFenceMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "perimeter",
            "in": "query",
            "required": true,
            "description": "Fence perimeter. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 1320
          },
          {
            "name": "animal",
            "in": "query",
            "required": false,
            "description": "Animal being contained.",
            "schema": {
              "type": "string",
              "enum": [
                "cattle",
                "deer",
                "hogs",
                "goats",
                "dogs",
                "poultry"
              ],
              "default": "cattle"
            }
          },
          {
            "name": "strands",
            "in": "query",
            "required": false,
            "description": "Strands (default by animal: cattle 3, deer 5, hogs 3, goats 5, dogs 2, poultry 4).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12
            }
          },
          {
            "name": "postSpacing",
            "in": "query",
            "required": false,
            "description": "Line post spacing. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 100 ft (30.48 m). Default 10 ft (≈3.048 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/fencing/electric",
                  "units": "imperial",
                  "input": {
                    "perimeter": 1320,
                    "animal": "cattle",
                    "postSpacing": 10
                  },
                  "result": {
                    "strands": 3,
                    "wire": 3960,
                    "wireRolls": 3,
                    "linePosts": 132,
                    "cornerPosts": 4,
                    "totalPosts": 136,
                    "insulators": 408,
                    "groundRods": 5,
                    "energizer": 1
                  },
                  "units_of_measure": {
                    "strands": "count",
                    "wire": "ft",
                    "wireRolls": "count",
                    "linePosts": "count",
                    "cornerPosts": "count",
                    "totalPosts": "count",
                    "insulators": "count",
                    "groundRods": "count",
                    "energizer": "J"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/fencing/acreage": {
      "get": {
        "operationId": "fencingAcreage",
        "tags": [
          "Fencing"
        ],
        "summary": "Fence length to enclose an acreage",
        "description": "Perimeter of a square (4√area) or a rectangle with a length:width ratio, for a given number of acres.\n\nSource: smartfencingcalculator.com (src/lib/calculations.ts) + DeckFenceMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "acres",
            "in": "query",
            "required": true,
            "description": "Area to enclose. Unit: acre (imperial) or ha (metric). Must be greater than 0. Max 100000 acre (40468.564 ha).",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 5
          },
          {
            "name": "shape",
            "in": "query",
            "required": false,
            "description": "Plot shape.",
            "schema": {
              "type": "string",
              "enum": [
                "square",
                "rectangle"
              ],
              "default": "square"
            }
          },
          {
            "name": "ratio",
            "in": "query",
            "required": false,
            "description": "Length ÷ width for a rectangle.",
            "schema": {
              "type": "number",
              "minimum": 1,
              "maximum": 50,
              "default": 2
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/fencing/acreage",
                  "units": "imperial",
                  "input": {
                    "acres": 5,
                    "shape": "square",
                    "ratio": 2
                  },
                  "result": {
                    "area": 217800,
                    "perimeter": 1866.7619,
                    "miles": 0.3536
                  },
                  "units_of_measure": {
                    "area": "sq ft",
                    "perimeter": "ft"
                  },
                  "notes": [
                    "Real lots are rarely perfect rectangles — verify with a survey."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/fencing/stain": {
      "get": {
        "operationId": "fencingStain",
        "tags": [
          "Fencing"
        ],
        "summary": "Fence stain gallons",
        "description": "Surface = length × height (× 2 for both sides); gallons = surface × coats ÷ coverage, whole cans.\n\nSource: smartfencingcalculator.com (src/lib/calculations.ts) + DeckFenceMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Fence length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 100
          },
          {
            "name": "height",
            "in": "query",
            "required": false,
            "description": "Fence height. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 16 ft (4.877 m). Default 6 ft (≈1.829 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 6
            }
          },
          {
            "name": "bothSides",
            "in": "query",
            "required": false,
            "description": "Stain both sides.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "coats",
            "in": "query",
            "required": false,
            "description": "Coats.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5,
              "default": 1
            }
          },
          {
            "name": "coveragePerGallon",
            "in": "query",
            "required": false,
            "description": "Coverage per gallon. Unit: sq ft/gal (imperial) or m²/L (metric). Must be greater than 0. Default 200 sq ft/gal (≈4.908 m²/L).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/fencing/stain",
                  "units": "imperial",
                  "input": {
                    "length": 100,
                    "height": 6,
                    "bothSides": true,
                    "coats": 1,
                    "coveragePerGallon": 200
                  },
                  "result": {
                    "surface": 1200,
                    "gallonCans": 6
                  },
                  "units_of_measure": {
                    "surface": "sq ft",
                    "gallonCans": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/drywall/sheets": {
      "get": {
        "operationId": "drywallSheets",
        "tags": [
          "Drywall"
        ],
        "summary": "Drywall sheets for an area",
        "description": "Sheets = ceil(net area × (1 + waste) ÷ sheet area). Openings under 32 sq ft are normally sheeted over and cut out, so pass the net area you actually cover.\n\nSource: Studio drywall app (DrywallMath) via InteriorMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Net area to cover. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 528
          },
          {
            "name": "sheet",
            "in": "query",
            "required": false,
            "description": "Sheet size (54x12 = 54\" × 12').",
            "schema": {
              "type": "string",
              "enum": [
                "4x8",
                "4x10",
                "4x12",
                "54x12"
              ],
              "default": "4x8"
            }
          },
          {
            "name": "thickness",
            "in": "query",
            "required": false,
            "description": "Board thickness in inches.",
            "schema": {
              "type": "string",
              "enum": [
                "1/4",
                "3/8",
                "1/2",
                "1/2-light",
                "5/8"
              ],
              "default": "1/2"
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/drywall/sheets",
                  "units": "imperial",
                  "input": {
                    "area": 528,
                    "sheet": "4x8",
                    "thickness": "1/2",
                    "wastePercent": 10
                  },
                  "result": {
                    "sheets": 19,
                    "sheetArea": 32,
                    "totalWeight": 969,
                    "screwLength": "1-1/4\""
                  },
                  "units_of_measure": {
                    "sheets": "count",
                    "sheetArea": "sq ft",
                    "totalWeight": "lb"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/drywall/room": {
      "get": {
        "operationId": "drywallRoom",
        "tags": [
          "Drywall"
        ],
        "summary": "Full drywall takeoff for a room",
        "description": "Walls (and optional ceiling) of a rectangular room: sheets, screws (≈32 per 4×8 wall sheet, 36 ceiling), joint compound by finish level, paper tape, corner bead and total board weight. Standard doors (20.4 sq ft) and windows (12 sq ft) are below the 32 sq ft deduction threshold and are sheeted over.\n\nSource: Studio drywall app (DrywallMath) via InteriorMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Room length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 12
          },
          {
            "name": "width",
            "in": "query",
            "required": true,
            "description": "Room width. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 12
          },
          {
            "name": "height",
            "in": "query",
            "required": false,
            "description": "Wall height. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 30 ft (9.144 m). Default 8 ft (≈2.438 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 8
            }
          },
          {
            "name": "ceiling",
            "in": "query",
            "required": false,
            "description": "Include the ceiling.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "doors",
            "in": "query",
            "required": false,
            "description": "Doors.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 50,
              "default": 1
            }
          },
          {
            "name": "windows",
            "in": "query",
            "required": false,
            "description": "Windows.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 50,
              "default": 2
            }
          },
          {
            "name": "sheet",
            "in": "query",
            "required": false,
            "description": "Sheet size (54x12 = 54\" × 12').",
            "schema": {
              "type": "string",
              "enum": [
                "4x8",
                "4x10",
                "4x12",
                "54x12"
              ],
              "default": "4x8"
            }
          },
          {
            "name": "thickness",
            "in": "query",
            "required": false,
            "description": "Board thickness in inches.",
            "schema": {
              "type": "string",
              "enum": [
                "1/4",
                "3/8",
                "1/2",
                "1/2-light",
                "5/8"
              ],
              "default": "1/2"
            }
          },
          {
            "name": "studSpacing",
            "in": "query",
            "required": false,
            "description": "Stud/joist spacing. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 24 in (609.6 mm). Default 16 in (≈406.4 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 16
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "finishLevel",
            "in": "query",
            "required": false,
            "description": "Finish level 0–5 (4 standard, 5 skim coat).",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 5,
              "default": 4
            }
          },
          {
            "name": "outsideCorners",
            "in": "query",
            "required": false,
            "description": "Outside corners needing bead.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/drywall/room",
                  "units": "imperial",
                  "input": {
                    "length": 12,
                    "width": 12,
                    "height": 8,
                    "ceiling": true,
                    "doors": 1,
                    "windows": 2,
                    "sheet": "4x8",
                    "thickness": "1/2",
                    "studSpacing": 16,
                    "wastePercent": 10,
                    "finishLevel": 4,
                    "outsideCorners": 0
                  },
                  "result": {
                    "wallArea": 384,
                    "ceilingArea": 144,
                    "deductedArea": 0,
                    "wallSheets": 14,
                    "ceilingSheets": 5,
                    "totalSheets": 19,
                    "screws": 567,
                    "screwBoxes1lb": 2,
                    "mud": 5.28,
                    "mudBuckets4_5gal": 1,
                    "mudBoxes3_5gal": 1,
                    "tapeRolls500ft": 1,
                    "cornerBead8ft": 0,
                    "totalWeight": 969
                  },
                  "units_of_measure": {
                    "wallArea": "sq ft",
                    "ceilingArea": "sq ft",
                    "deductedArea": "sq ft",
                    "wallSheets": "count",
                    "ceilingSheets": "count",
                    "totalSheets": "count",
                    "screws": "count",
                    "screwBoxes1lb": "count",
                    "mud": "US gal",
                    "mudBuckets4_5gal": "count",
                    "mudBoxes3_5gal": "count",
                    "tapeRolls500ft": "count",
                    "cornerBead8ft": "count",
                    "totalWeight": "lb"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/drywall/finishing": {
      "get": {
        "operationId": "drywallFinishing",
        "tags": [
          "Drywall"
        ],
        "summary": "Joint compound, tape and corner bead",
        "description": "Ready-mix compound ≈ 1 gal per 100 sq ft at level 4 (levels 0–5 scale; 5 doubles for a skim coat), packed into 4.5 gal buckets then 3.5 gal boxes; seams 0.55 LF/sq ft hung horizontally (0.65 vertical); paper tape 500 ft or mesh 300 ft rolls.\n\nSource: Studio drywall app (DrywallMath) via InteriorMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Drywall area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 528
          },
          {
            "name": "finishLevel",
            "in": "query",
            "required": false,
            "description": "Finish level 0–5.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 5,
              "default": 4
            }
          },
          {
            "name": "orientation",
            "in": "query",
            "required": false,
            "description": "Sheet orientation.",
            "schema": {
              "type": "string",
              "enum": [
                "horizontal",
                "vertical"
              ],
              "default": "horizontal"
            }
          },
          {
            "name": "tape",
            "in": "query",
            "required": false,
            "description": "Tape type.",
            "schema": {
              "type": "string",
              "enum": [
                "paper",
                "mesh"
              ],
              "default": "paper"
            }
          },
          {
            "name": "cornerLengths",
            "in": "query",
            "required": false,
            "description": "Comma-separated lengths of outside corners (each corner rounds up to whole 8 ft sticks). Unit: ft (imperial) or m (metric). Comma-separated numbers, e.g. 8,8,9.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/drywall/finishing",
                  "units": "imperial",
                  "input": {
                    "area": 528,
                    "finishLevel": 4,
                    "orientation": "horizontal",
                    "tape": "paper",
                    "cornerLengths": []
                  },
                  "result": {
                    "mud": 5.28,
                    "mudBuckets4_5gal": 1,
                    "mudBoxes3_5gal": 1,
                    "seamLength": 290.4,
                    "tapeRolls": 1,
                    "cornerBead8ft": 0
                  },
                  "units_of_measure": {
                    "mud": "US gal",
                    "mudBuckets4_5gal": "count",
                    "mudBoxes3_5gal": "count",
                    "seamLength": "ft",
                    "tapeRolls": "count",
                    "cornerBead8ft": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/drywall/screws": {
      "get": {
        "operationId": "drywallScrews",
        "tags": [
          "Drywall"
        ],
        "summary": "Drywall screws per sheet and total",
        "description": "Screws per sheet = studs crossed × rows (16\" rows on walls, 12\" on ceilings); ~320 screws per lb.\n\nSource: Studio drywall app (DrywallMath) via InteriorMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "sheets",
            "in": "query",
            "required": true,
            "description": "Number of sheets.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000
            },
            "example": 19
          },
          {
            "name": "sheet",
            "in": "query",
            "required": false,
            "description": "Sheet size (54x12 = 54\" × 12').",
            "schema": {
              "type": "string",
              "enum": [
                "4x8",
                "4x10",
                "4x12",
                "54x12"
              ],
              "default": "4x8"
            }
          },
          {
            "name": "studSpacing",
            "in": "query",
            "required": false,
            "description": "Stud/joist spacing. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 24 in (609.6 mm). Default 16 in (≈406.4 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 16
            }
          },
          {
            "name": "ceiling",
            "in": "query",
            "required": false,
            "description": "Ceiling sheets (tighter pattern).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/drywall/screws",
                  "units": "imperial",
                  "input": {
                    "sheets": 19,
                    "sheet": "4x8",
                    "studSpacing": 16,
                    "ceiling": false
                  },
                  "result": {
                    "screwsPerSheet": 28,
                    "screws": 532,
                    "weight": 1.6625,
                    "boxes1lb": 2
                  },
                  "units_of_measure": {
                    "screwsPerSheet": "count",
                    "screws": "count",
                    "weight": "lb",
                    "boxes1lb": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/insulation/blown-in": {
      "get": {
        "operationId": "insulationBlownIn",
        "tags": [
          "Insulation"
        ],
        "summary": "Blown-in insulation bags to reach a target R-value",
        "description": "Credits existing insulation, then settled depth = R gap ÷ R per inch; blow-to depth adds settling (cellulose 15%); bags come from the coverage chart (sq-ft-inches per bag ÷ settled depth), not naive volume.\n\nSource: Studio insulation app via InteriorMath.kt; smart-insulation-guide. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Attic floor area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 1000
          },
          {
            "name": "targetR",
            "in": "query",
            "required": false,
            "description": "Target R-value (R-38 to R-60 for most US attics). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 49
            }
          },
          {
            "name": "material",
            "in": "query",
            "required": false,
            "description": "Loose-fill material.",
            "schema": {
              "type": "string",
              "enum": [
                "cellulose",
                "fiberglass",
                "mineral-wool"
              ],
              "default": "cellulose"
            }
          },
          {
            "name": "existingDepth",
            "in": "query",
            "required": false,
            "description": "Depth of existing insulation. Unit: in (imperial) or mm (metric). Max 36 in (914.4 mm). Default 0 in (≈0 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "existingType",
            "in": "query",
            "required": false,
            "description": "Existing insulation type.",
            "schema": {
              "type": "string",
              "enum": [
                "fiberglass-loose",
                "fiberglass-batt",
                "cellulose",
                "rock-wool"
              ],
              "default": "fiberglass-loose"
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/insulation/blown-in",
                  "units": "imperial",
                  "input": {
                    "area": 1000,
                    "targetR": 49,
                    "material": "cellulose",
                    "existingDepth": 0,
                    "existingType": "fiberglass-loose",
                    "wastePercent": 0
                  },
                  "result": {
                    "existingR": 0,
                    "addR": 49,
                    "settledDepth": 14,
                    "installedDepth": 16.1,
                    "coveragePerBag": 25,
                    "bags": 40,
                    "totalWeight": 1000,
                    "finishedR": 49
                  },
                  "units_of_measure": {
                    "existingR": "R (ft²·°F·h/BTU)",
                    "addR": "R (ft²·°F·h/BTU)",
                    "settledDepth": "in",
                    "installedDepth": "in",
                    "coveragePerBag": "sq ft",
                    "bags": "count",
                    "totalWeight": "lb",
                    "finishedR": "R (ft²·°F·h/BTU)"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/insulation/batts": {
      "get": {
        "operationId": "insulationBatts",
        "tags": [
          "Insulation"
        ],
        "summary": "Wall batt bundles",
        "description": "Net wall area (length × height − openings) × (1 + waste) ÷ sq ft per bundle from the product table (R-13 to R-38, 15\" for 16\" OC or 23\" for 24\" OC framing).\n\nSource: Studio insulation app via InteriorMath.kt; smart-insulation-guide. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Wall length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 40
          },
          {
            "name": "height",
            "in": "query",
            "required": false,
            "description": "Wall height. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 30 ft (9.144 m). Default 8 ft (≈2.438 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 8
            }
          },
          {
            "name": "rValue",
            "in": "query",
            "required": false,
            "description": "Batt R-value.",
            "schema": {
              "type": "string",
              "enum": [
                "13",
                "15",
                "19",
                "21",
                "30",
                "38"
              ],
              "default": "13"
            }
          },
          {
            "name": "studSpacing",
            "in": "query",
            "required": false,
            "description": "Stud spacing (in).",
            "schema": {
              "type": "string",
              "enum": [
                "16",
                "24"
              ],
              "default": "16"
            }
          },
          {
            "name": "windows",
            "in": "query",
            "required": false,
            "description": "Windows.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100,
              "default": 0
            }
          },
          {
            "name": "windowArea",
            "in": "query",
            "required": false,
            "description": "Area of one window. Unit: sq ft (imperial) or m² (metric). Default 15 sq ft (≈1.394 m²).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 15
            }
          },
          {
            "name": "doors",
            "in": "query",
            "required": false,
            "description": "Doors.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100,
              "default": 0
            }
          },
          {
            "name": "doorArea",
            "in": "query",
            "required": false,
            "description": "Area of one door. Unit: sq ft (imperial) or m² (metric). Default 20 sq ft (≈1.858 m²).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 20
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/insulation/batts",
                  "units": "imperial",
                  "input": {
                    "length": 40,
                    "height": 8,
                    "rValue": "13",
                    "studSpacing": "16",
                    "windows": 0,
                    "windowArea": 15,
                    "doors": 0,
                    "doorArea": 20,
                    "wastePercent": 5
                  },
                  "result": {
                    "grossArea": 320,
                    "openings": 0,
                    "netArea": 320,
                    "areaWithWaste": 336,
                    "battWidth": 15,
                    "cavity": "2x4",
                    "coveragePerBundle": 97,
                    "bundles": 4
                  },
                  "units_of_measure": {
                    "grossArea": "sq ft",
                    "openings": "sq ft",
                    "netArea": "sq ft",
                    "areaWithWaste": "sq ft",
                    "battWidth": "in",
                    "coveragePerBundle": "sq ft",
                    "bundles": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/insulation/r-value-thickness": {
      "get": {
        "operationId": "insulationRValueThickness",
        "tags": [
          "Insulation"
        ],
        "summary": "Thickness and units to add for a target R-value",
        "description": "R still needed = target − existing; thickness = R ÷ R-per-inch by product (blown fiberglass 2.5, cellulose 3.2, fiberglass batt 3.1, mineral batt 3.3, open-cell 3.7, closed-cell 6.5); bags at 22 sq ft, batts at 48 sq ft, spray kits at 200.\n\nSource: smartroofingcalculator.com (calcInsulation). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Area to insulate. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 1000
          },
          {
            "name": "targetR",
            "in": "query",
            "required": false,
            "description": "Target R-value. Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 49
            }
          },
          {
            "name": "existingR",
            "in": "query",
            "required": false,
            "description": "Existing R-value.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100,
              "default": 0
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Product type.",
            "schema": {
              "type": "string",
              "enum": [
                "blown-fiberglass",
                "blown-cellulose",
                "batt-fiberglass",
                "batt-mineral",
                "spray-open",
                "spray-closed"
              ],
              "default": "blown-fiberglass"
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 5
            }
          },
          {
            "name": "costPerSqFt",
            "in": "query",
            "required": false,
            "description": "Installed price per area (optional). Unit: currency/sq ft (imperial) or currency/m² (metric).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/insulation/r-value-thickness",
                  "units": "imperial",
                  "input": {
                    "area": 1000,
                    "targetR": 49,
                    "existingR": 0,
                    "type": "blown-fiberglass",
                    "wastePercent": 5
                  },
                  "result": {
                    "rNeeded": 49,
                    "thickness": 19.6,
                    "coverageArea": 1050,
                    "units": 48,
                    "unitType": "bags",
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "rNeeded": "R (ft²·°F·h/BTU)",
                    "thickness": "in",
                    "coverageArea": "sq ft",
                    "units": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tile/count": {
      "get": {
        "operationId": "tileCount",
        "tags": [
          "Tile"
        ],
        "summary": "Tiles and boxes for an area",
        "description": "Tiles = ceil(area × (1 + waste) ÷ tile face area); boxes from tiles per box or sq ft per box. Waste defaults by layout pattern (straight 10%, diagonal 15%, herringbone 18%, basketweave 15%, mosaic 5%).\n\nSource: smarttilecalculator.com (src/lib/tileCalculations.ts) + InteriorMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Net area to tile. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 80
          },
          {
            "name": "tileWidth",
            "in": "query",
            "required": false,
            "description": "Tile width. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 96 in (2438.4 mm). Default 12 in (≈304.8 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 12
            }
          },
          {
            "name": "tileLength",
            "in": "query",
            "required": false,
            "description": "Tile length. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 96 in (2438.4 mm). Default 24 in (≈609.6 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 24
            }
          },
          {
            "name": "pattern",
            "in": "query",
            "required": false,
            "description": "Layout pattern (sets default waste).",
            "schema": {
              "type": "string",
              "enum": [
                "straight",
                "offset-third",
                "offset-half",
                "diagonal",
                "herringbone",
                "basketweave",
                "mosaic"
              ],
              "default": "straight"
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Override the pattern waste. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 60,
              "default": 0
            }
          },
          {
            "name": "tilesPerBox",
            "in": "query",
            "required": false,
            "description": "Tiles per box (optional).",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "sqFtPerBox",
            "in": "query",
            "required": false,
            "description": "Coverage per box (optional; used when tilesPerBox is not given). Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/tile/count",
                  "units": "imperial",
                  "input": {
                    "area": 80,
                    "tileWidth": 12,
                    "tileLength": 24,
                    "pattern": "straight",
                    "wastePercent": 0
                  },
                  "result": {
                    "wastePercent": 10,
                    "netArea": 80,
                    "areaWithWaste": 88,
                    "tileArea": 2,
                    "tiles": 44,
                    "boxes": null
                  },
                  "units_of_measure": {
                    "wastePercent": "%",
                    "netArea": "sq ft",
                    "areaWithWaste": "sq ft",
                    "tileArea": "sq ft",
                    "tiles": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tile/grout": {
      "get": {
        "operationId": "tileGrout",
        "tags": [
          "Tile"
        ],
        "summary": "Grout pounds, bags and coverage per bag",
        "description": "Exact joint geometry: each tile sits in a (W+J)×(L+J) repeat cell; joint volume per sq ft × density (sanded 110, unsanded 100, epoxy 125 lb/ft³). Joint depth defaults to the tile thickness. Also returns sq ft one bag covers.\n\nSource: smarttilecalculator.com (src/lib/tileCalculations.ts) + InteriorMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Tiled area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 100
          },
          {
            "name": "tileWidth",
            "in": "query",
            "required": false,
            "description": "Tile width. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 96 in (2438.4 mm). Default 12 in (≈304.8 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 12
            }
          },
          {
            "name": "tileLength",
            "in": "query",
            "required": false,
            "description": "Tile length. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 96 in (2438.4 mm). Default 24 in (≈609.6 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 24
            }
          },
          {
            "name": "tileThickness",
            "in": "query",
            "required": false,
            "description": "Tile thickness. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 2 in (50.8 mm). Default 0.375 in (≈9.525 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0.375
            }
          },
          {
            "name": "jointWidth",
            "in": "query",
            "required": false,
            "description": "Grout joint width. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 1 in (25.4 mm). Default 0.125 in (≈3.175 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 0.125
            }
          },
          {
            "name": "jointDepth",
            "in": "query",
            "required": false,
            "description": "Joint depth (defaults to tile thickness). Unit: in (imperial) or mm (metric). Must be greater than 0. Max 2 in (50.8 mm).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          },
          {
            "name": "groutType",
            "in": "query",
            "required": false,
            "description": "Grout type (use unsanded under 1/8\").",
            "schema": {
              "type": "string",
              "enum": [
                "sanded",
                "unsanded",
                "epoxy"
              ],
              "default": "sanded"
            }
          },
          {
            "name": "bagSize",
            "in": "query",
            "required": false,
            "description": "Bag size. Unit: lb (imperial) or kg (metric). Must be greater than 0. Max 100 lb (45.359 kg). Default 25 lb (≈11.34 kg).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/tile/grout",
                  "units": "imperial",
                  "input": {
                    "area": 100,
                    "tileWidth": 12,
                    "tileLength": 24,
                    "tileThickness": 0.375,
                    "jointWidth": 0.125,
                    "groutType": "sanded",
                    "bagSize": 25
                  },
                  "result": {
                    "grout": 5.3,
                    "bags": 1,
                    "coveragePerBag": 471
                  },
                  "units_of_measure": {
                    "grout": "lb",
                    "bags": "count",
                    "coveragePerBag": "sq ft"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tile/thinset": {
      "get": {
        "operationId": "tileThinset",
        "tags": [
          "Tile"
        ],
        "summary": "Thinset mortar bags by trowel notch",
        "description": "50 lb bags of thinset at the notch's coverage (3/16 V 110, 1/4×1/4 95, 1/4 U 85, 1/4×3/8 75, 3/8×3/8 60, 1/2×1/2 45 sq ft). The notch defaults from the tile's long edge; large-format tile (15\"+) adds a back-butter bag per 135 sq ft.\n\nSource: smarttilecalculator.com (src/lib/tileCalculations.ts) + InteriorMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Net area (you trowel the floor, not the offcuts). Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 80
          },
          {
            "name": "tileWidth",
            "in": "query",
            "required": false,
            "description": "Tile width. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 96 in (2438.4 mm). Default 12 in (≈304.8 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 12
            }
          },
          {
            "name": "tileLength",
            "in": "query",
            "required": false,
            "description": "Tile length. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 96 in (2438.4 mm). Default 24 in (≈609.6 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 24
            }
          },
          {
            "name": "notch",
            "in": "query",
            "required": false,
            "description": "Trowel notch (defaults from tile size).",
            "schema": {
              "type": "string",
              "enum": [
                "3/16-v",
                "1/4x1/4-square",
                "1/4-u",
                "1/4x3/8-square",
                "3/8x3/8-square",
                "1/2x1/2-square"
              ]
            }
          },
          {
            "name": "backButter",
            "in": "query",
            "required": false,
            "description": "Back-butter: auto = yes when the long edge is 15\"+ (large format).",
            "schema": {
              "type": "string",
              "enum": [
                "auto",
                "yes",
                "no"
              ],
              "default": "auto"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/tile/thinset",
                  "units": "imperial",
                  "input": {
                    "area": 80,
                    "tileWidth": 12,
                    "tileLength": 24,
                    "backButter": "auto"
                  },
                  "result": {
                    "notch": "1/2x1/2-square",
                    "coveragePerBag": 45,
                    "trowelBags50lb": 2,
                    "backButterBags50lb": 1,
                    "totalBags50lb": 3
                  },
                  "units_of_measure": {
                    "coveragePerBag": "sq ft",
                    "trowelBags50lb": "count",
                    "backButterBags50lb": "count",
                    "totalBags50lb": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tile/backer-board": {
      "get": {
        "operationId": "tileBackerBoard",
        "tags": [
          "Tile"
        ],
        "summary": "Cement backer board sheets and screws",
        "description": "3×5 ft sheets (15 sq ft) with 10% for cuts; 30 screws per sheet.\n\nSource: smarttilecalculator.com (src/lib/tileCalculations.ts) + InteriorMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Area to cover. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 80
          },
          {
            "name": "sheet",
            "in": "query",
            "required": false,
            "description": "Sheet size (ft).",
            "schema": {
              "type": "string",
              "enum": [
                "3x5",
                "4x8"
              ],
              "default": "3x5"
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Cut allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/tile/backer-board",
                  "units": "imperial",
                  "input": {
                    "area": 80,
                    "sheet": "3x5",
                    "wastePercent": 10
                  },
                  "result": {
                    "sheets": 6,
                    "screws": 180,
                    "seamTape": 32
                  },
                  "units_of_measure": {
                    "sheets": "count",
                    "screws": "count",
                    "seamTape": "ft"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/tile/cost": {
      "get": {
        "operationId": "tileCost",
        "tags": [
          "Tile"
        ],
        "summary": "Tile job cost (material band + supplies + labor)",
        "description": "Tile material low/high by type on the area with waste, setting supplies at $2.25/sq ft, and labor at $10/sq ft × a regional multiplier (0 for DIY).\n\nSource: smarttilecalculator.com (src/lib/tileCalculations.ts) + InteriorMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Area to tile. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 100
          },
          {
            "name": "material",
            "in": "query",
            "required": false,
            "description": "Tile material.",
            "schema": {
              "type": "string",
              "enum": [
                "ceramic",
                "porcelain",
                "natural-stone",
                "glass-mosaic"
              ],
              "default": "porcelain"
            }
          },
          {
            "name": "region",
            "in": "query",
            "required": false,
            "description": "Labor region.",
            "schema": {
              "type": "string",
              "enum": [
                "national",
                "northeast",
                "west-coast",
                "midwest",
                "south",
                "rural"
              ],
              "default": "national"
            }
          },
          {
            "name": "diy",
            "in": "query",
            "required": false,
            "description": "DIY install (no labor).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 60,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/tile/cost",
                  "units": "imperial",
                  "input": {
                    "area": 100,
                    "material": "porcelain",
                    "region": "national",
                    "diy": false,
                    "wastePercent": 10
                  },
                  "result": {
                    "tileLow": 330,
                    "tileHigh": 1100,
                    "supplies": 225,
                    "labor": 1000,
                    "totalLow": 1555,
                    "totalHigh": 2325,
                    "perAreaLow": 15.55,
                    "perAreaHigh": 23.25
                  },
                  "units_of_measure": {
                    "tileLow": "currency",
                    "tileHigh": "currency",
                    "supplies": "currency",
                    "labor": "currency",
                    "totalLow": "currency",
                    "totalHigh": "currency",
                    "perAreaLow": "currency/sq ft",
                    "perAreaHigh": "currency/sq ft"
                  },
                  "notes": [
                    "US 2026 national planning ranges."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/landscape/gravel": {
      "get": {
        "operationId": "landscapeGravel",
        "tags": [
          "Landscape & Aggregates"
        ],
        "summary": "Gravel / crushed stone: cubic yards, tons, bags, truckloads, cost",
        "description": "Area × depth → cubic yards, × (1 + waste) (and optional compaction for crusher run 15%, DG 12%) → tons by stone density, 50 lb bags (0.5 cu ft), truckloads and a delivered material cost band.\n\nSource: smartgravelcalculator.com (src/lib/calculators*.ts) + YardMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "shape",
            "in": "query",
            "required": false,
            "description": "Area shape (circle: length = diameter; triangle: length = base, width = height).",
            "schema": {
              "type": "string",
              "enum": [
                "rectangle",
                "circle",
                "triangle"
              ],
              "default": "rectangle"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": false,
            "description": "Length (or diameter / base). Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 20
          },
          {
            "name": "width",
            "in": "query",
            "required": false,
            "description": "Width (or triangle height). Ignored for circles. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 12
          },
          {
            "name": "area",
            "in": "query",
            "required": false,
            "description": "Area — give this instead of length/width. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          },
          {
            "name": "depth",
            "in": "query",
            "required": false,
            "description": "Depth. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 48 in (1219.2 mm). Default 4 in (≈101.6 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 4
            }
          },
          {
            "name": "material",
            "in": "query",
            "required": false,
            "description": "Stone type.",
            "schema": {
              "type": "string",
              "enum": [
                "crusher-run",
                "stone-57",
                "stone-8",
                "stone-4",
                "pea-gravel",
                "river-rock",
                "decomposed-granite",
                "limestone",
                "marble-chips",
                "sand"
              ],
              "default": "crusher-run"
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste / settling allowance (10% standard, 15% soft ground). Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "compaction",
            "in": "query",
            "required": false,
            "description": "Also add the stone's compaction allowance.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "truck",
            "in": "query",
            "required": false,
            "description": "Truck size for load count (pickup 0.5 t, single 5, tandem 10, triaxle 15).",
            "schema": {
              "type": "string",
              "enum": [
                "pickup",
                "single",
                "tandem",
                "triaxle"
              ],
              "default": "tandem"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/landscape/gravel",
                  "units": "imperial",
                  "input": {
                    "shape": "rectangle",
                    "length": 20,
                    "width": 12,
                    "depth": 4,
                    "material": "crusher-run",
                    "wastePercent": 10,
                    "compaction": false,
                    "truck": "tandem"
                  },
                  "result": {
                    "area": 240,
                    "volumeBeforeWaste": 2.963,
                    "volume": 3.2593,
                    "volumeCubicFeet": 88,
                    "weight": 4.8889,
                    "weightPounds": 9777.7778,
                    "tonsToOrder": 4.9,
                    "bags50lb": 176,
                    "truckloads": 1,
                    "coveragePerTon": 54,
                    "costLow": 107.5556,
                    "costHigh": 195.5556,
                    "density": 1.5
                  },
                  "units_of_measure": {
                    "area": "sq ft",
                    "volumeBeforeWaste": "cu yd",
                    "volume": "cu yd",
                    "weight": "US ton",
                    "bags50lb": "count",
                    "truckloads": "count",
                    "coveragePerTon": "sq ft/ton",
                    "costLow": "currency",
                    "costHigh": "currency",
                    "density": "US ton/cu yd"
                  },
                  "notes": [
                    "At this volume bulk delivery is far cheaper than bagged stone."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/landscape/mulch": {
      "get": {
        "operationId": "landscapeMulch",
        "tags": [
          "Landscape & Aggregates"
        ],
        "summary": "Mulch cubic yards, bags and bulk vs bagged cost",
        "description": "Area × depth → cubic yards and bags (2 or 3 cu ft), with bulk (incl. delivery) and bagged cost bands by mulch type.\n\nSource: smartgravelcalculator.com (src/lib/calculators*.ts) + YardMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "shape",
            "in": "query",
            "required": false,
            "description": "Area shape (circle: length = diameter; triangle: length = base, width = height).",
            "schema": {
              "type": "string",
              "enum": [
                "rectangle",
                "circle",
                "triangle"
              ],
              "default": "rectangle"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": false,
            "description": "Length (or diameter / base). Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 20
          },
          {
            "name": "width",
            "in": "query",
            "required": false,
            "description": "Width (or triangle height). Ignored for circles. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 12
          },
          {
            "name": "area",
            "in": "query",
            "required": false,
            "description": "Area — give this instead of length/width. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          },
          {
            "name": "depth",
            "in": "query",
            "required": false,
            "description": "Depth (2–3 in typical). Unit: in (imperial) or mm (metric). Must be greater than 0. Max 12 in (304.8 mm). Default 3 in (≈76.2 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 3
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Mulch type.",
            "schema": {
              "type": "string",
              "enum": [
                "hardwood",
                "dyed",
                "pine",
                "cedar",
                "cypress",
                "rubber"
              ],
              "default": "hardwood"
            }
          },
          {
            "name": "bagSize",
            "in": "query",
            "required": false,
            "description": "Bag size (cu ft).",
            "schema": {
              "type": "string",
              "enum": [
                "2",
                "3"
              ],
              "default": "2"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/landscape/mulch",
                  "units": "imperial",
                  "input": {
                    "shape": "rectangle",
                    "length": 20,
                    "width": 12,
                    "depth": 3,
                    "type": "hardwood",
                    "bagSize": "2"
                  },
                  "result": {
                    "area": 240,
                    "volume": 2.2222,
                    "volumeCubicFeet": 60,
                    "bags": 30,
                    "bulkCostLow": 111.6667,
                    "bulkCostHigh": 195,
                    "bagCostLow": 105,
                    "bagCostHigh": 150
                  },
                  "units_of_measure": {
                    "area": "sq ft",
                    "volume": "cu yd",
                    "bags": "count",
                    "bulkCostLow": "currency",
                    "bulkCostHigh": "currency",
                    "bagCostLow": "currency",
                    "bagCostHigh": "currency"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/landscape/topsoil": {
      "get": {
        "operationId": "landscapeTopsoil",
        "tags": [
          "Landscape & Aggregates"
        ],
        "summary": "Topsoil cubic yards, tons and bags",
        "description": "Area × depth × (1 + settling) → cubic yards; tons by grade (screened 1.1, blend 1.0, fill 1.2 t/yd³); 0.75 and 1 cu ft bags; cost band.\n\nSource: smartgravelcalculator.com (src/lib/calculators*.ts) + YardMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "shape",
            "in": "query",
            "required": false,
            "description": "Area shape (circle: length = diameter; triangle: length = base, width = height).",
            "schema": {
              "type": "string",
              "enum": [
                "rectangle",
                "circle",
                "triangle"
              ],
              "default": "rectangle"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": false,
            "description": "Length (or diameter / base). Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 20
          },
          {
            "name": "width",
            "in": "query",
            "required": false,
            "description": "Width (or triangle height). Ignored for circles. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 12
          },
          {
            "name": "area",
            "in": "query",
            "required": false,
            "description": "Area — give this instead of length/width. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          },
          {
            "name": "depth",
            "in": "query",
            "required": false,
            "description": "Depth. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 36 in (914.4 mm). Default 4 in (≈101.6 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 4
            }
          },
          {
            "name": "grade",
            "in": "query",
            "required": false,
            "description": "Soil grade.",
            "schema": {
              "type": "string",
              "enum": [
                "screened",
                "blend",
                "fill"
              ],
              "default": "screened"
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Settling allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/landscape/topsoil",
                  "units": "imperial",
                  "input": {
                    "shape": "rectangle",
                    "length": 20,
                    "width": 12,
                    "depth": 4,
                    "grade": "screened",
                    "wastePercent": 10
                  },
                  "result": {
                    "area": 240,
                    "volume": 3.2593,
                    "weight": 3.5852,
                    "bags0_75cuft": 118,
                    "bags1cuft": 88,
                    "costLow": 65.1852,
                    "costHigh": 162.963
                  },
                  "units_of_measure": {
                    "area": "sq ft",
                    "volume": "cu yd",
                    "weight": "US ton",
                    "bags0_75cuft": "count",
                    "bags1cuft": "count",
                    "costLow": "currency",
                    "costHigh": "currency"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/landscape/sand": {
      "get": {
        "operationId": "landscapeSand",
        "tags": [
          "Landscape & Aggregates"
        ],
        "summary": "Sand cubic yards, tons and 50 lb bags",
        "description": "Area × depth × (1 + waste) → cubic yards → tons by sand type (fill, mason, concrete, play, arena) → 50 lb bags; cost band.\n\nSource: smartgravelcalculator.com (src/lib/calculators*.ts) + YardMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "shape",
            "in": "query",
            "required": false,
            "description": "Area shape (circle: length = diameter; triangle: length = base, width = height).",
            "schema": {
              "type": "string",
              "enum": [
                "rectangle",
                "circle",
                "triangle"
              ],
              "default": "rectangle"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": false,
            "description": "Length (or diameter / base). Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 20
          },
          {
            "name": "width",
            "in": "query",
            "required": false,
            "description": "Width (or triangle height). Ignored for circles. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 12
          },
          {
            "name": "area",
            "in": "query",
            "required": false,
            "description": "Area — give this instead of length/width. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          },
          {
            "name": "depth",
            "in": "query",
            "required": false,
            "description": "Depth. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 36 in (914.4 mm). Default 2 in (≈50.8 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 2
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Sand type.",
            "schema": {
              "type": "string",
              "enum": [
                "fill",
                "mason",
                "concrete",
                "play",
                "arena"
              ],
              "default": "concrete"
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/landscape/sand",
                  "units": "imperial",
                  "input": {
                    "shape": "rectangle",
                    "length": 20,
                    "width": 12,
                    "depth": 2,
                    "type": "concrete",
                    "wastePercent": 10
                  },
                  "result": {
                    "area": 240,
                    "volume": 1.6296,
                    "weight": 2.2815,
                    "bags50lb": 92,
                    "costLow": 57.037,
                    "costHigh": 109.5111
                  },
                  "units_of_measure": {
                    "area": "sq ft",
                    "volume": "cu yd",
                    "weight": "US ton",
                    "bags50lb": "count",
                    "costLow": "currency",
                    "costHigh": "currency"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/landscape/sod": {
      "get": {
        "operationId": "landscapeSod",
        "tags": [
          "Landscape & Aggregates"
        ],
        "summary": "Sod rolls/slabs, pallets, weight and trips",
        "description": "Area × (1 + waste) ÷ piece size (2×5 ft roll 10 sq ft, 16×24 in slab exactly 2⅔ sq ft, big roll 420 sq ft); pallets at 500 sq ft; ~2,500 lb per pallet; pickup trips.\n\nSource: Studio sod app (SodMath) via YardMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Lawn area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 2450
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "piece",
            "in": "query",
            "required": false,
            "description": "Piece format.",
            "schema": {
              "type": "string",
              "enum": [
                "roll-2x5",
                "slab-16x24",
                "big-roll"
              ],
              "default": "roll-2x5"
            }
          },
          {
            "name": "palletCoverage",
            "in": "query",
            "required": false,
            "description": "Coverage per pallet. Unit: sq ft (imperial) or m² (metric). Must be greater than 0. Default 500 sq ft (≈46.452 m²).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 500
            }
          },
          {
            "name": "palletWeight",
            "in": "query",
            "required": false,
            "description": "Weight per pallet. Unit: lb (imperial) or kg (metric). Must be greater than 0. Default 2500 lb (≈1133.981 kg).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 2500
            }
          },
          {
            "name": "palletsPerTrip",
            "in": "query",
            "required": false,
            "description": "Pallets per vehicle trip (1 for a half-ton pickup).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 30,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/landscape/sod",
                  "units": "imperial",
                  "input": {
                    "area": 2450,
                    "wastePercent": 10,
                    "piece": "roll-2x5",
                    "palletCoverage": 500,
                    "palletWeight": 2500,
                    "palletsPerTrip": 1
                  },
                  "result": {
                    "areaWithWaste": 2695,
                    "pieces": 270,
                    "pallets": 6,
                    "leftoverPieces": 30,
                    "weight": 7.5,
                    "trips": 6
                  },
                  "units_of_measure": {
                    "areaWithWaste": "sq ft",
                    "pieces": "count",
                    "pallets": "count",
                    "leftoverPieces": "count",
                    "weight": "US ton",
                    "trips": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/landscape/grass-seed": {
      "get": {
        "operationId": "landscapeGrassSeed",
        "tags": [
          "Landscape & Aggregates"
        ],
        "summary": "Grass seed pounds and bags by species",
        "description": "lb per 1,000 sq ft by species for a new lawn (overseeding is half), bags, and germination window.\n\nSource: Studio sod app (SodMath) via YardMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Area to seed. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 5000
          },
          {
            "name": "species",
            "in": "query",
            "required": false,
            "description": "Grass species.",
            "schema": {
              "type": "string",
              "enum": [
                "tall-fescue",
                "kentucky-bluegrass",
                "perennial-rye",
                "fine-fescue",
                "bermuda",
                "zoysia",
                "bahia",
                "centipede"
              ],
              "default": "tall-fescue"
            }
          },
          {
            "name": "overseed",
            "in": "query",
            "required": false,
            "description": "Overseeding an existing lawn (half rate).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "bagSize",
            "in": "query",
            "required": false,
            "description": "Bag size. Unit: lb (imperial) or kg (metric). Must be greater than 0. Max 100 lb (45.359 kg). Default 5 lb (≈2.268 kg).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/landscape/grass-seed",
                  "units": "imperial",
                  "input": {
                    "area": 5000,
                    "species": "tall-fescue",
                    "overseed": false,
                    "bagSize": 5
                  },
                  "result": {
                    "ratePer1000SqFt": 8,
                    "seed": 40,
                    "bags": 8,
                    "germinationDays": {
                      "min": 7,
                      "max": 12
                    }
                  },
                  "units_of_measure": {
                    "ratePer1000SqFt": "lb",
                    "seed": "lb",
                    "bags": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/concrete/slab": {
      "get": {
        "operationId": "concreteSlab",
        "tags": [
          "Concrete"
        ],
        "summary": "Concrete slab: cubic yards, bags, gravel base, ready-mix vs bags",
        "description": "Slab volume + 8% over-pour → cubic yards and 40/60/80 lb bags (0.30/0.45/0.60 cu ft), a crusher-run base (+20% compaction) in tons, and ready-mix ($160–210/yd, short-load fee under 4 yd) vs bagged cost.\n\nSource: smartgravelcalculator.com (src/lib/calculators*.ts) + YardMath.kt. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Slab length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 10
          },
          {
            "name": "width",
            "in": "query",
            "required": true,
            "description": "Slab width. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 10
          },
          {
            "name": "thickness",
            "in": "query",
            "required": false,
            "description": "Slab thickness. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 24 in (609.6 mm). Default 4 in (≈101.6 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 4
            }
          },
          {
            "name": "baseDepth",
            "in": "query",
            "required": false,
            "description": "Gravel base depth. Unit: in (imperial) or mm (metric). Max 24 in (609.6 mm). Default 4 in (≈101.6 mm).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 4
            }
          },
          {
            "name": "bagSize",
            "in": "query",
            "required": false,
            "description": "Bag size (lb).",
            "schema": {
              "type": "string",
              "enum": [
                "40",
                "60",
                "80"
              ],
              "default": "80"
            }
          },
          {
            "name": "overPourPercent",
            "in": "query",
            "required": false,
            "description": "Over-pour / waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 30,
              "default": 8
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/concrete/slab",
                  "units": "imperial",
                  "input": {
                    "length": 10,
                    "width": 10,
                    "thickness": 4,
                    "baseDepth": 4,
                    "bagSize": "80",
                    "overPourPercent": 8
                  },
                  "result": {
                    "area": 100,
                    "volumeBeforeOverPour": 1.2346,
                    "volume": 1.3333,
                    "volumeCubicFeet": 36,
                    "bags": 60,
                    "readyMixCostLow": 393.3333,
                    "readyMixCostHigh": 580,
                    "bagCostLow": 330,
                    "bagCostHigh": 450,
                    "baseVolume": 1.4815,
                    "baseWeight": 2.2222
                  },
                  "units_of_measure": {
                    "area": "sq ft",
                    "volumeBeforeOverPour": "cu yd",
                    "volume": "cu yd",
                    "bags": "count",
                    "readyMixCostLow": "currency",
                    "readyMixCostHigh": "currency",
                    "bagCostLow": "currency",
                    "bagCostHigh": "currency",
                    "baseVolume": "cu yd",
                    "baseWeight": "US ton"
                  },
                  "notes": [
                    "Compact the gravel base before setting forms."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/concrete/volume": {
      "get": {
        "operationId": "concreteVolume",
        "tags": [
          "Concrete"
        ],
        "summary": "Concrete for slabs, footings, round columns or holes → yards and bags",
        "description": "Volume for a rectangular pour (length × width × depth) or round columns/holes (π r² × depth × count), plus waste, in cubic yards and bags.\n\nSource: DeckFenceMath.kt (DeckFootings) + smartgravelcalculator.com. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "shape",
            "in": "query",
            "required": false,
            "description": "Pour shape.",
            "schema": {
              "type": "string",
              "enum": [
                "rectangle",
                "round"
              ],
              "default": "rectangle"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": false,
            "description": "Length (rectangle). Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 20
          },
          {
            "name": "width",
            "in": "query",
            "required": false,
            "description": "Width (rectangle). Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 2
          },
          {
            "name": "depth",
            "in": "query",
            "required": true,
            "description": "Depth / thickness / column height. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 240 in (6096 mm).",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 12
          },
          {
            "name": "diameter",
            "in": "query",
            "required": false,
            "description": "Diameter (round). Unit: in (imperial) or mm (metric). Must be greater than 0. Max 120 in (3048 mm).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          },
          {
            "name": "count",
            "in": "query",
            "required": false,
            "description": "Number of identical pours.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000,
              "default": 1
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 30,
              "default": 5
            }
          },
          {
            "name": "bagSize",
            "in": "query",
            "required": false,
            "description": "Bag size (lb).",
            "schema": {
              "type": "string",
              "enum": [
                "40",
                "60",
                "80"
              ],
              "default": "80"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/concrete/volume",
                  "units": "imperial",
                  "input": {
                    "shape": "rectangle",
                    "length": 20,
                    "width": 2,
                    "depth": 12,
                    "count": 1,
                    "wastePercent": 5,
                    "bagSize": "80"
                  },
                  "result": {
                    "volumeBeforeWaste": 1.4815,
                    "volume": 1.5556,
                    "volumeCubicFeet": 42,
                    "bags": 70,
                    "weight": 6090
                  },
                  "units_of_measure": {
                    "volumeBeforeWaste": "cu yd",
                    "volume": "cu yd",
                    "bags": "count",
                    "weight": "lb"
                  },
                  "notes": [
                    "Normal-weight concrete ≈ 145 lb/cu ft."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/paint/room": {
      "get": {
        "operationId": "paintRoom",
        "tags": [
          "Paint"
        ],
        "summary": "Paint and primer gallons for a room",
        "description": "Net wall area = perimeter × height − doors (21 sq ft) − windows (15 sq ft), optional ceiling; gallons = area × coats × (1 + waste) ÷ coverage (350–400 sq ft/gal on the label), whole gallons, 5-gal buckets, and primer at one coat.\n\nSource: API-authored (label-coverage method). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Room length. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 14
          },
          {
            "name": "width",
            "in": "query",
            "required": true,
            "description": "Room width. Unit: ft (imperial) or m (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 12
          },
          {
            "name": "height",
            "in": "query",
            "required": false,
            "description": "Wall height. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 30 ft (9.144 m). Default 8 ft (≈2.438 m).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 8
            }
          },
          {
            "name": "doors",
            "in": "query",
            "required": false,
            "description": "Doors.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 50,
              "default": 1
            }
          },
          {
            "name": "windows",
            "in": "query",
            "required": false,
            "description": "Windows.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 50,
              "default": 2
            }
          },
          {
            "name": "coats",
            "in": "query",
            "required": false,
            "description": "Coats.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5,
              "default": 2
            }
          },
          {
            "name": "coveragePerGallon",
            "in": "query",
            "required": false,
            "description": "Coverage per gallon. Unit: sq ft/gal (imperial) or m²/L (metric). Must be greater than 0. Max 600 sq ft/gal (14.725 m²/L). Default 350 sq ft/gal (≈8.59 m²/L).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "default": 350
            }
          },
          {
            "name": "includeCeiling",
            "in": "query",
            "required": false,
            "description": "Paint the ceiling too.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "primer",
            "in": "query",
            "required": false,
            "description": "Also estimate primer (one coat).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "wastePercent",
            "in": "query",
            "required": false,
            "description": "Waste / touch-up allowance. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 50,
              "default": 0
            }
          },
          {
            "name": "pricePerGallon",
            "in": "query",
            "required": false,
            "description": "Price per gallon (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/paint/room",
                  "units": "imperial",
                  "input": {
                    "length": 14,
                    "width": 12,
                    "height": 8,
                    "doors": 1,
                    "windows": 2,
                    "coats": 2,
                    "coveragePerGallon": 350,
                    "includeCeiling": false,
                    "primer": false,
                    "wastePercent": 0
                  },
                  "result": {
                    "wallArea": 365,
                    "ceilingArea": 0,
                    "paintableArea": 365,
                    "gallonsExact": 2.0857,
                    "gallonCans": 3,
                    "fiveGallonBuckets": 0,
                    "extraGallonCans": 3,
                    "quartsIfUnderOneGallon": 0,
                    "primerGallonCans": 0,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "wallArea": "sq ft",
                    "ceilingArea": "sq ft",
                    "paintableArea": "sq ft",
                    "gallonsExact": "US gal",
                    "gallonCans": "count",
                    "fiveGallonBuckets": "count",
                    "extraGallonCans": "count",
                    "quartsIfUnderOneGallon": "count",
                    "primerGallonCans": "count"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/convert/units": {
      "get": {
        "operationId": "convertUnits",
        "tags": [
          "Conversions"
        ],
        "summary": "Convert length, area, volume, mass or temperature",
        "description": "Converts between construction units. Length: mm cm m km in ft yd mi. Area: mm2 cm2 m2 ha km2 in2 ft2 yd2 acre square (roofing square = 100 ft²). Volume: ml l m3 in3 ft3 yd3 gal qt board-ft. Mass: g kg t oz lb ton (US short ton). Temperature: c f k.\n\nSource: ConversionFormulas.kt (studio converter), exact SI factors. Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "value",
            "in": "query",
            "required": true,
            "description": "Value to convert.",
            "schema": {
              "type": "number"
            },
            "example": 12
          },
          {
            "name": "from",
            "in": "query",
            "required": true,
            "description": "Unit to convert from.",
            "schema": {
              "type": "string",
              "enum": [
                "mm",
                "cm",
                "m",
                "km",
                "in",
                "ft",
                "yd",
                "mi",
                "mm2",
                "cm2",
                "m2",
                "ha",
                "km2",
                "in2",
                "ft2",
                "yd2",
                "acre",
                "square",
                "ml",
                "l",
                "m3",
                "in3",
                "ft3",
                "yd3",
                "gal",
                "qt",
                "board-ft",
                "g",
                "kg",
                "t",
                "oz",
                "lb",
                "ton",
                "c",
                "f",
                "k"
              ]
            },
            "example": "ft"
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "description": "Unit to convert to.",
            "schema": {
              "type": "string",
              "enum": [
                "mm",
                "cm",
                "m",
                "km",
                "in",
                "ft",
                "yd",
                "mi",
                "mm2",
                "cm2",
                "m2",
                "ha",
                "km2",
                "in2",
                "ft2",
                "yd2",
                "acre",
                "square",
                "ml",
                "l",
                "m3",
                "in3",
                "ft3",
                "yd3",
                "gal",
                "qt",
                "board-ft",
                "g",
                "kg",
                "t",
                "oz",
                "lb",
                "ton",
                "c",
                "f",
                "k"
              ]
            },
            "example": "m"
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/convert/units",
                  "units": "imperial",
                  "input": {
                    "value": 12,
                    "from": "ft",
                    "to": "m"
                  },
                  "result": {
                    "category": "length",
                    "value": 12,
                    "from": "ft",
                    "to": "m",
                    "result": 3.6576
                  },
                  "units_of_measure": {},
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/convert/pitch": {
      "get": {
        "operationId": "convertPitch",
        "tags": [
          "Conversions"
        ],
        "summary": "Roof pitch ↔ degrees ↔ percent slope",
        "description": "Give exactly one of pitch (rise per 12), degrees, or percent; returns all three plus the pitch factor and hip/valley factor.\n\nSource: smartroofingcalculator.com (calcPitch). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "pitch",
            "in": "query",
            "required": false,
            "description": "Rise in inches per 12 in of run.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 48
            },
            "example": 6
          },
          {
            "name": "degrees",
            "in": "query",
            "required": false,
            "description": "Angle in degrees.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 89
            }
          },
          {
            "name": "percent",
            "in": "query",
            "required": false,
            "description": "Slope in percent. Percent (10 = 10%).",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 400
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/convert/pitch",
                  "units": "imperial",
                  "input": {
                    "pitch": 6
                  },
                  "result": {
                    "pitch": 6,
                    "pitchLabel": "6/12",
                    "degrees": 26.5651,
                    "percent": 50,
                    "pitchFactor": 1.118,
                    "hipValleyFactor": 1.5
                  },
                  "units_of_measure": {
                    "degrees": "°",
                    "percent": "%"
                  },
                  "notes": [
                    "Moderate slope — standard shingles suitable."
                  ],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/convert/board-feet": {
      "get": {
        "operationId": "convertBoardFeet",
        "tags": [
          "Conversions"
        ],
        "summary": "Lumber board feet and linear feet",
        "description": "Board feet = nominal thickness (in) × nominal width (in) × length (ft) ÷ 12 × pieces. Also returns total linear feet and cost when a price per board foot is given.\n\nSource: API-authored (standard lumber board-foot rule). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "thickness",
            "in": "query",
            "required": true,
            "description": "Nominal thickness in inches (lumber is sold by nominal inches, so this is never unit-converted). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 24
            },
            "example": 2
          },
          {
            "name": "width",
            "in": "query",
            "required": true,
            "description": "Nominal width in inches. Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 48
            },
            "example": 6
          },
          {
            "name": "length",
            "in": "query",
            "required": true,
            "description": "Piece length. Unit: ft (imperial) or m (metric). Must be greater than 0. Max 60 ft (18.288 m).",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 12
          },
          {
            "name": "pieces",
            "in": "query",
            "required": false,
            "description": "Number of pieces.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000000,
              "default": 1
            }
          },
          {
            "name": "pricePerBoardFoot",
            "in": "query",
            "required": false,
            "description": "Price per board foot (optional).",
            "schema": {
              "type": "number",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/convert/board-feet",
                  "units": "imperial",
                  "input": {
                    "thickness": 2,
                    "width": 6,
                    "length": 12,
                    "pieces": 1
                  },
                  "result": {
                    "boardFeet": 12,
                    "linearFeet": 12,
                    "estimatedCost": null
                  },
                  "units_of_measure": {
                    "linearFeet": "ft"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/convert/volume-from-area": {
      "get": {
        "operationId": "convertVolumeFromArea",
        "tags": [
          "Conversions"
        ],
        "summary": "Area × depth → cubic feet, cubic yards, cubic meters",
        "description": "The core fill formula: area × (depth ÷ 12) ÷ 27 = cubic yards. Useful for any bulk material.\n\nSource: smartgravelcalculator.com (cubicYards). Also accepts POST with the same parameters as a JSON body.",
        "parameters": [
          {
            "name": "units",
            "in": "query",
            "required": false,
            "description": "Unit system for dimensioned inputs and outputs. Counts and money are never converted.",
            "schema": {
              "type": "string",
              "enum": [
                "imperial",
                "metric"
              ],
              "default": "imperial"
            }
          },
          {
            "name": "area",
            "in": "query",
            "required": true,
            "description": "Area. Unit: sq ft (imperial) or m² (metric). Must be greater than 0.",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 240
          },
          {
            "name": "depth",
            "in": "query",
            "required": true,
            "description": "Depth. Unit: in (imperial) or mm (metric). Must be greater than 0. Max 240 in (6096 mm).",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "example": 4
          }
        ],
        "responses": {
          "200": {
            "description": "Calculation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Result"
                },
                "example": {
                  "endpoint": "/v1/convert/volume-from-area",
                  "units": "imperial",
                  "input": {
                    "area": 240,
                    "depth": 4
                  },
                  "result": {
                    "cubicFeet": 80,
                    "cubicYards": 2.963,
                    "cubicMeters": 2.2653
                  },
                  "units_of_measure": {
                    "cubicFeet": "cu ft",
                    "cubicYards": "cu yd"
                  },
                  "notes": [],
                  "warnings": [],
                  "requestId": "b7e0c2a4-5f7e-4c47-9a53-8f3a0c1d2e9b"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or missing parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status": 400,
                    "code": "INVALID_PARAMETER",
                    "message": "'length' must be greater than 0 ft",
                    "details": []
                  },
                  "requestId": "3f1c…"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/endpoints": {
      "get": {
        "operationId": "listEndpoints",
        "tags": [
          "Meta"
        ],
        "summary": "List every endpoint",
        "description": "Machine-readable catalog of all calculators.",
        "responses": {
          "200": {
            "description": "Catalog",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/health": {
      "get": {
        "operationId": "health",
        "tags": [
          "Meta"
        ],
        "summary": "Health check",
        "description": "Returns ok and the API version. Free, not counted against quota by most plans.",
        "responses": {
          "200": {
            "description": "Healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "ok",
                  "version": "1.0.0",
                  "endpoints": 65
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Result": {
        "type": "object",
        "required": [
          "endpoint",
          "units",
          "input",
          "result",
          "units_of_measure",
          "notes",
          "warnings",
          "requestId"
        ],
        "properties": {
          "endpoint": {
            "type": "string",
            "description": "The endpoint that answered."
          },
          "units": {
            "type": "string",
            "enum": [
              "imperial",
              "metric"
            ]
          },
          "input": {
            "type": "object",
            "description": "Accepted inputs after defaults, in the caller's units.",
            "additionalProperties": true
          },
          "result": {
            "type": "object",
            "description": "Calculated values.",
            "additionalProperties": true
          },
          "units_of_measure": {
            "type": "object",
            "description": "Unit label for each dimensioned field of `result` (dotted path → label).",
            "additionalProperties": {
              "type": "string"
            }
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "e.g. unknown parameters that were ignored."
          },
          "requestId": {
            "type": "string"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "requestId"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "status",
              "code",
              "message",
              "details"
            ],
            "properties": {
              "status": {
                "type": "integer"
              },
              "code": {
                "type": "string",
                "enum": [
                  "INVALID_PARAMETER",
                  "MISSING_PARAMETER",
                  "INVALID_JSON",
                  "PAYLOAD_TOO_LARGE",
                  "NOT_FOUND",
                  "METHOD_NOT_ALLOWED",
                  "UNAUTHORIZED",
                  "RATE_LIMITED",
                  "SERVICE_NOT_CONFIGURED",
                  "INTERNAL_ERROR"
                ]
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "param": {
                      "type": "string"
                    },
                    "issue": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "requestId": {
            "type": "string"
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Request did not come through RapidAPI",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
