{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Config",
  "description": "Top-level Plumb configuration.\n\n`Eq` is not derived — several sub-structs carry `f32` fields.",
  "type": "object",
  "properties": {
    "viewports": {
      "description": "Named viewports to snapshot the page at.",
      "type": "object",
      "additionalProperties": {
        "$ref": "#/$defs/ViewportSpec"
      },
      "default": {}
    },
    "spacing": {
      "description": "Spacing spec — the allowed discrete values for `gap`, `margin`,\n`padding`, etc.",
      "$ref": "#/$defs/SpacingSpec",
      "default": {
        "base_unit": 4,
        "scale": [],
        "tokens": {}
      }
    },
    "type": {
      "description": "Type scale spec.",
      "$ref": "#/$defs/TypeScaleSpec",
      "default": {
        "families": [],
        "weights": [],
        "scale": [],
        "tokens": {}
      }
    },
    "color": {
      "description": "Color palette spec.",
      "$ref": "#/$defs/ColorSpec",
      "default": {
        "tokens": {},
        "delta_e_tolerance": 2.0
      }
    },
    "radius": {
      "description": "Border-radius spec.",
      "$ref": "#/$defs/RadiusSpec",
      "default": {
        "scale": []
      }
    },
    "alignment": {
      "description": "Alignment / layout spec.",
      "$ref": "#/$defs/AlignmentSpec",
      "default": {
        "grid_columns": null,
        "gutter_px": null,
        "tolerance_px": 3
      }
    },
    "shadow": {
      "description": "Box-shadow spec.",
      "$ref": "#/$defs/ShadowSpec",
      "default": {
        "scale": []
      }
    },
    "z_index": {
      "description": "Z-index spec.",
      "$ref": "#/$defs/ZIndexSpec",
      "default": {
        "scale": []
      }
    },
    "opacity": {
      "description": "Opacity spec.",
      "$ref": "#/$defs/OpacitySpec",
      "default": {
        "scale": []
      }
    },
    "rhythm": {
      "description": "Vertical rhythm spec.",
      "$ref": "#/$defs/RhythmSpec",
      "default": {
        "base_line_px": 0,
        "tolerance_px": 2,
        "cap_height_fallback_px": 0
      }
    },
    "a11y": {
      "description": "Accessibility spec.",
      "$ref": "#/$defs/A11ySpec",
      "default": {
        "min_contrast_ratio": null,
        "touch_target": {
          "min_width_px": 24,
          "min_height_px": 24
        }
      }
    },
    "rules": {
      "description": "Per-rule overrides — severity bumps, enable/disable.",
      "type": "object",
      "additionalProperties": {
        "$ref": "#/$defs/RuleOverride"
      },
      "default": {}
    },
    "ignore": {
      "description": "Selector-scoped runtime suppressions.\n\nEach entry suppresses every violation whose `selector` field\nmatches `selector` exactly (no CSS-engine match — exact string\nequality only). When `rule_id` is set, the suppression is\nfurther constrained to that single rule; when `rule_id` is\nabsent, every rule fired at that selector is suppressed.\n\nThe list MUST be applied **after** rule evaluation: the\nmatched violations are partitioned out of the reported set and\ncounted under `ignored`, never silently dropped.",
      "type": "array",
      "items": {
        "$ref": "#/$defs/IgnoreRule"
      }
    }
  },
  "additionalProperties": false,
  "$defs": {
    "ViewportSpec": {
      "description": "Specification of a single named viewport.",
      "type": "object",
      "properties": {
        "width": {
          "description": "Width in CSS pixels.",
          "type": "integer",
          "format": "uint32",
          "minimum": 0
        },
        "height": {
          "description": "Height in CSS pixels.",
          "type": "integer",
          "format": "uint32",
          "minimum": 0
        },
        "device_pixel_ratio": {
          "description": "Device pixel ratio. Defaults to 1.0.",
          "type": "number",
          "format": "float",
          "default": 1.0
        }
      },
      "additionalProperties": false,
      "required": [
        "width",
        "height"
      ]
    },
    "SpacingSpec": {
      "description": "Spacing spec.",
      "type": "object",
      "properties": {
        "base_unit": {
          "description": "Base unit in pixels; discrete scale is multiples of this.",
          "type": "integer",
          "format": "uint32",
          "minimum": 0,
          "default": 4
        },
        "scale": {
          "description": "Allowed spacing values in pixels.",
          "type": "array",
          "items": {
            "type": "integer",
            "format": "uint32",
            "minimum": 0
          },
          "default": []
        },
        "tokens": {
          "description": "Named tokens mapped to their pixel values.",
          "type": "object",
          "additionalProperties": {
            "type": "integer",
            "format": "uint32",
            "minimum": 0
          },
          "default": {}
        }
      },
      "additionalProperties": false
    },
    "TypeScaleSpec": {
      "description": "Type scale spec.",
      "type": "object",
      "properties": {
        "families": {
          "description": "Allowed font families.",
          "type": "array",
          "items": {
            "type": "string"
          },
          "default": []
        },
        "weights": {
          "description": "Allowed font weights.",
          "type": "array",
          "items": {
            "type": "integer",
            "format": "uint16",
            "minimum": 0,
            "maximum": 65535
          },
          "default": []
        },
        "scale": {
          "description": "Allowed font sizes in pixels.",
          "type": "array",
          "items": {
            "type": "integer",
            "format": "uint32",
            "minimum": 0
          },
          "default": []
        },
        "tokens": {
          "description": "Named type tokens mapped to their pixel values.",
          "type": "object",
          "additionalProperties": {
            "type": "integer",
            "format": "uint32",
            "minimum": 0
          },
          "default": {}
        }
      },
      "additionalProperties": false
    },
    "ColorSpec": {
      "description": "Color spec.\n\nTokens are flat name → hex pairs. Slash-delimited names\n(`\"bg/canvas\"`, `\"fg/primary\"`) namespace the palette without\nrequiring nested tables — TOML quotes the key, the rule engine\ntreats the slash as a hint for grouping in diagnostics.",
      "type": "object",
      "properties": {
        "tokens": {
          "description": "Named tokens mapped to hex values (e.g. `#0b7285`). Slash-delimited\nkeys (`\"bg/canvas\"`) act as informal namespaces.",
          "type": "object",
          "additionalProperties": {
            "type": "string"
          },
          "default": {}
        },
        "delta_e_tolerance": {
          "description": "CIEDE2000 Delta-E tolerance when matching off-palette colors.",
          "type": "number",
          "format": "float",
          "default": 2.0
        }
      },
      "additionalProperties": false
    },
    "RadiusSpec": {
      "description": "Border-radius spec.",
      "type": "object",
      "properties": {
        "scale": {
          "description": "Allowed border-radius values in pixels.\n\nNaming matches `spacing.scale` and `type.scale` for consistency.",
          "type": "array",
          "items": {
            "type": "integer",
            "format": "uint32",
            "minimum": 0
          },
          "default": []
        }
      },
      "additionalProperties": false
    },
    "AlignmentSpec": {
      "description": "Alignment / layout spec.",
      "type": "object",
      "properties": {
        "grid_columns": {
          "description": "Grid column count, if the design uses a fixed grid.",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint32",
          "minimum": 0,
          "default": null
        },
        "gutter_px": {
          "description": "Container gutter in pixels.",
          "type": [
            "integer",
            "null"
          ],
          "format": "uint32",
          "minimum": 0,
          "default": null
        },
        "tolerance_px": {
          "description": "Edge-clustering tolerance in pixels for `edge/near-alignment`.\nDefaults to 3 px.",
          "type": "integer",
          "format": "uint32",
          "minimum": 0,
          "default": 3
        }
      },
      "additionalProperties": false
    },
    "ShadowSpec": {
      "description": "Box-shadow spec.",
      "type": "object",
      "properties": {
        "scale": {
          "description": "Allowed box-shadow values. Each entry is a complete shadow\nexpression as returned by `getComputedStyle`.",
          "type": "array",
          "items": {
            "type": "string"
          },
          "default": []
        }
      },
      "additionalProperties": false
    },
    "ZIndexSpec": {
      "description": "Z-index spec.",
      "type": "object",
      "properties": {
        "scale": {
          "description": "Allowed z-index values.",
          "type": "array",
          "items": {
            "type": "integer",
            "format": "int32"
          },
          "default": []
        }
      },
      "additionalProperties": false
    },
    "OpacitySpec": {
      "description": "Opacity spec.",
      "type": "object",
      "properties": {
        "scale": {
          "description": "Allowed opacity values in the range `[0.0, 1.0]`.",
          "type": "array",
          "items": {
            "type": "number",
            "format": "float"
          },
          "default": []
        }
      },
      "additionalProperties": false
    },
    "RhythmSpec": {
      "description": "Vertical-rhythm spec.",
      "type": "object",
      "properties": {
        "base_line_px": {
          "description": "Base line-height in pixels.",
          "type": "integer",
          "format": "uint32",
          "minimum": 0,
          "default": 0
        },
        "tolerance_px": {
          "description": "Tolerance in pixels for rhythm checks.",
          "type": "integer",
          "format": "uint32",
          "minimum": 0,
          "default": 2
        },
        "cap_height_fallback_px": {
          "description": "Cap-height fallback in pixels when font metrics are unavailable.",
          "type": "integer",
          "format": "uint32",
          "minimum": 0,
          "default": 0
        }
      },
      "additionalProperties": false
    },
    "A11ySpec": {
      "description": "Accessibility spec.",
      "type": "object",
      "properties": {
        "min_contrast_ratio": {
          "description": "Minimum contrast ratio to enforce (e.g. `4.5` for WCAG AA body text).",
          "type": [
            "number",
            "null"
          ],
          "format": "float",
          "default": null
        },
        "touch_target": {
          "description": "Minimum interactive-element size for `a11y/touch-target`.",
          "$ref": "#/$defs/TouchTargetSpec",
          "default": {
            "min_width_px": 24,
            "min_height_px": 24
          }
        }
      },
      "additionalProperties": false
    },
    "TouchTargetSpec": {
      "description": "Touch-target threshold per WCAG 2.5.8 (Target Size, Minimum).\n\nDefaults to 24×24 CSS pixels.",
      "type": "object",
      "properties": {
        "min_width_px": {
          "description": "Minimum interactive width in CSS pixels.",
          "type": "integer",
          "format": "uint32",
          "minimum": 0,
          "default": 24
        },
        "min_height_px": {
          "description": "Minimum interactive height in CSS pixels.",
          "type": "integer",
          "format": "uint32",
          "minimum": 0,
          "default": 24
        }
      },
      "additionalProperties": false
    },
    "RuleOverride": {
      "description": "Per-rule override.",
      "type": "object",
      "properties": {
        "enabled": {
          "description": "Enable or disable the rule entirely.",
          "type": "boolean",
          "default": true
        },
        "severity": {
          "description": "Override the rule's default severity.",
          "anyOf": [
            {
              "$ref": "#/$defs/Severity"
            },
            {
              "type": "null"
            }
          ],
          "default": null
        }
      },
      "additionalProperties": false
    },
    "Severity": {
      "description": "How severe a violation is. Maps to CLI exit-code thresholds and to the\nSARIF `level` field.",
      "oneOf": [
        {
          "description": "Suggestion — ignored by default CI thresholds.",
          "type": "string",
          "const": "info"
        },
        {
          "description": "Warning — CI-configurable.",
          "type": "string",
          "const": "warning"
        },
        {
          "description": "Error — fails CI by default.",
          "type": "string",
          "const": "error"
        }
      ]
    },
    "IgnoreRule": {
      "description": "A single selector-scoped suppression entry.\n\nMirrors the shape `plumb lint --suggest-ignores` emits, so a user\ncan pipe the suggestion list back into `plumb.toml` and converge on\na clean dogfood run without further editing.",
      "type": "object",
      "properties": {
        "selector": {
          "description": "Exact CSS-selector path (`SnapshotNode::selector`) to suppress.\nString equality only; this is **not** a CSS engine match.",
          "type": "string"
        },
        "rule_id": {
          "description": "Optional rule identifier (e.g. `spacing/grid-conformance`). When\n`Some`, the suppression only applies to that rule. When `None`,\nevery rule's violation at `selector` is suppressed.",
          "type": [
            "string",
            "null"
          ],
          "default": null
        },
        "reason": {
          "description": "Required human-readable justification. Documents why the\nselector is exempt so the next reviewer understands the intent.",
          "type": "string"
        }
      },
      "additionalProperties": false,
      "required": [
        "selector",
        "reason"
      ]
    }
  }
}