# Visual Design Rules
# Machine-enforceable rules for evaluator pipelines.
# Consumed by www evaluator via @verdigristech/design-tokens/rules/visual-rules.yml
#
# Version: 2.1.0
# Last updated: 2026-04-10
#
# Schema: visual-rules/2.1.0
# Every enforceable rule entry MUST have:
#   id:          unique dot-path identifier (e.g., color.contrast.normal-text)
#   severity:    "error" | "warning"
#   type:        "value" | "pattern" | "constraint" | "reference"
#   description: human-readable explanation
#   test:        machine-testable field — one of:
#     regex:     regex pattern to match/flag in source
#     min/max:   numeric bounds
#     value:     exact required value
#     values:    set of allowed values

# =============================================================================
# COLOR
# =============================================================================
color:
  # ---------------------------------------------------------------------------
  # Brand teal
  # ---------------------------------------------------------------------------
  brand_teal:
    id: "color.brand-teal"
    type: "value"
    oklch: "oklch(0.75 0.1286 191.57)"
    hex: "#0fc8c3"
    hsl: "hsl(178, 86%, 42%)"
    usage: "Decorative brand color — backgrounds, illustrations, data viz"
    never_as_text_on_white: true   # Fails WCAG AA (contrast ~2.9:1 on white)
    ok_on_dark_backgrounds: true   # Passes AA on neutral.950 and darker
    min_background_lightness: 0.35 # OKLch L* of background must be <= this for teal text
    rules:
      - id: "color.brand-teal.no-text-on-white"
        description: "Brand teal fails WCAG AA (~2.9:1) as text on white backgrounds"
        severity: "error"
        type: "constraint"
        test:
          max: 0.35              # OKLch L* of background for teal text
      - id: "color.brand-teal.decorative-only-light"
        description: "Brand teal on light backgrounds is decorative only — not for text"
        severity: "error"
        type: "constraint"

  # ---------------------------------------------------------------------------
  # Brand gradient stops (for reference — enforce order only)
  # ---------------------------------------------------------------------------
  brand_gradient:
    id: "color.brand-gradient"
    type: "reference"
    description: "Teal -> Purple -> Red -> Yellow -> Teal loop. 16 stops."
    start: "oklch(0.75 0.1286 191.57)"   # brand.verdigris
    end: "oklch(0.87 0.1786 92.23)"       # brand.cyber-yellow
    enforce_order: true  # Gradient stops must follow hue rotation

  # ---------------------------------------------------------------------------
  # WCAG contrast requirements per context
  # ---------------------------------------------------------------------------
  contrast:
    normal_text:
      id: "color.contrast.normal-text"
      severity: "error"
      type: "constraint"
      min_ratio: 4.5
      description: "WCAG AA for text < 18px bold or < 24px regular"
      test:
        min: 4.5
    large_text:
      id: "color.contrast.large-text"
      severity: "error"
      type: "constraint"
      min_ratio: 3.0
      description: "WCAG AA for text >= 18px bold or >= 24px regular"
      test:
        min: 3.0
    ui_components:
      id: "color.contrast.ui-components"
      severity: "error"
      type: "constraint"
      min_ratio: 3.0
      description: "WCAG AA for interactive UI components and graphical objects"
      test:
        min: 3.0
    decorative:
      id: "color.contrast.decorative"
      type: "reference"
      min_ratio: 0
      description: "No contrast requirement for purely decorative elements"
    enhanced_normal_text:
      id: "color.contrast.enhanced"
      severity: "warning"
      type: "constraint"
      min_ratio: 7.0
      description: "WCAG AAA for normal text (aspirational, not blocking)"
      test:
        min: 7.0

  # ---------------------------------------------------------------------------
  # Dark mode requirements
  # ---------------------------------------------------------------------------
  dark_mode:
    id: "color.dark-mode"
    type: "value"
    background:
      value: "oklch(0.141 0.005 285.823)"   # neutral.950
      description: "Near-black, NOT pure black. Pure black (#000) is banned."
      never_pure_black: true
    card_background:
      value: "oklch(0.21 0.006 285.885)"     # neutral.900
    border:
      description: "Semi-transparent borders in dark mode for layering"
      pattern: "border-color:\\s*oklch\\([^)]+\\s*/\\s*[\\d.]+\\)"
      recommended_opacity_range: [0.1, 0.3]
    text_foreground:
      value: "oklch(0.985 0 0)"              # neutral.50
    muted_foreground:
      value: "oklch(0.552 0.016 285.938)"    # neutral.500

  # ---------------------------------------------------------------------------
  # Banned colors — flag if found in source
  # ---------------------------------------------------------------------------
  banned_colors:
    id: "color.banned"
    type: "pattern"
    severity: "error"
    description: "Flag banned color literals in source — use design tokens instead"
    items:
      - value: "#ff0000"
        reason: "Pure red — use status.destructive-light or destructive-dark"
      - value: "#00ff00"
        reason: "Pure green — off-brand"
      - value: "#0000ff"
        reason: "Pure blue — off-brand"
      - value: "#000000"
        reason: "Pure black — use neutral.950 oklch(0.141 0.005 285.823)"
        context: "background"  # Only banned as background; OK in SVG strokes
      - value: "#ffffff"
        reason: "Prefer neutral.50 for foreground in dark mode"
        context: "dark_mode_text"
        severity: "warning"
      - value: "rgb(0, 0, 255)"
        reason: "Pure blue — off-brand"
      - value: "rgb(255, 0, 0)"
        reason: "Pure red — use status tokens"
      - value: "rgb(0, 255, 0)"
        reason: "Pure green — off-brand"
      - value: "#ff00ff"
        reason: "Magenta — off-brand"
      - value: "#800080"
        reason: "Generic purple — use brand.midnight-purple"
      - value: "#ffa500"
        reason: "Generic orange — off-brand"
      - value: "#008000"
        reason: "Generic green — off-brand"

  # ---------------------------------------------------------------------------
  # Neutral palette (for rule cross-referencing)
  # ---------------------------------------------------------------------------
  neutrals:
    id: "color.neutrals"
    type: "reference"
    description: "Neutral palette for rule cross-referencing"
    white: "oklch(1 0 0)"
    "50": "oklch(0.985 0 0)"
    "100": "oklch(0.967 0.001 286.375)"
    "200": "oklch(0.92 0.004 286.32)"
    "400": "oklch(0.705 0.015 286.067)"
    "500": "oklch(0.552 0.016 285.938)"
    "800": "oklch(0.274 0.006 286.033)"
    "900": "oklch(0.21 0.006 285.885)"
    "950": "oklch(0.141 0.005 285.823)"
    black: "oklch(0 0 0)"

  # ---------------------------------------------------------------------------
  # Status colors
  # ---------------------------------------------------------------------------
  status:
    id: "color.status"
    type: "reference"
    description: "Status color values for rule cross-referencing"
    destructive_light: "oklch(0.577 0.245 27.325)"
    destructive_dark: "oklch(0.704 0.191 22.216)"

  # ---------------------------------------------------------------------------
  # Palette semantic roles — machine-consumable color usage guidance
  # Maps palette regions to content categories for AI agents and evaluators.
  # The design system defines what colors MEAN, not just what they ARE.
  # ---------------------------------------------------------------------------
  palette_semantics:
    id: "color.palette-semantics"
    type: "reference"
    description: "Maps brand palette regions to content categories and usage contexts. AI agents and evaluators use this to select contextually appropriate colors for page sections, accents, and data visualization."

    # --- Palette regions (anchor + steps) mapped to content meaning ---
    regions:
      trust:
        id: "color.palette-semantics.trust"
        type: "reference"
        description: "Technical authority, reliability, core brand identity. The primary Verdigris register."
        tokens: ["color.brand.verdigris", "color.brand.mix-step-1", "color.brand.mix-step-2"]
        hue_range: "191°–240°"
        lightness_range: "0.52–0.75"
        content_categories:
          - "technology and platform capabilities"
          - "data quality and validation"
          - "primary brand expression"
          - "charts and data visualization (primary series)"
        usage:
          section_accent: "Background tints, icon backgrounds, decorative borders"
          text: "Only on dark backgrounds (brand teal fails WCAG on white)"
          dark_mode_tint: "oklch(0.75 0.1286 191.57 / 15%) over neutral.950"

      depth:
        id: "color.palette-semantics.depth"
        type: "reference"
        description: "Intelligence, sophistication, premium. The deep end of the palette — works exceptionally in dark mode."
        tokens: ["color.brand.mix-step-3", "color.brand.midnight-purple"]
        hue_range: "265°–289°"
        lightness_range: "0.29–0.41"
        content_categories:
          - "AI and intelligence features"
          - "premium or enterprise sections"
          - "dark hero backgrounds and overlays"
          - "footer and deep navigation"
        usage:
          section_accent: "Dark section backgrounds, hero overlays, gradient endpoints"
          text: "NOT as text — too dark (L=0.29-0.41), invisible on dark backgrounds"
          dark_mode_tint: "oklch(0.29 0.1506 289.33 / 18%) over neutral.950 — use 18%+ for perceptibility"
        constraints:
          min_tint_opacity: 0.15
          reason: "Below 15% opacity, midnight-purple (chroma 0.15) is indistinguishable from neutral gray"

      energy:
        id: "color.palette-semantics.energy"
        type: "reference"
        description: "Urgency, detection, alerting. The vibrant middle of the palette with high chroma."
        tokens: ["color.brand.midnight-purple-step-1", "color.brand.midnight-purple-step-2", "color.brand.midnight-purple-step-3"]
        hue_range: "313°–0°"
        lightness_range: "0.39–0.60"
        content_categories:
          - "fault detection and alerting"
          - "real-time monitoring"
          - "anomaly and signal indicators"
          - "active states and attention-drawing UI"
        usage:
          section_accent: "Accent borders, icon tints, alert section backgrounds"
          text: "mp-step-2 (L=0.50) and mp-step-3 (L=0.60) work as text on both light and dark backgrounds"
          dark_mode_tint: "oklch(0.495 0.1708 336.72 / 12%) over neutral.950"

      warmth:
        id: "color.palette-semantics.warmth"
        type: "reference"
        description: "Human connection, action, engagement. Warm tones that invite interaction."
        tokens: ["color.brand.pastel-red", "color.brand.pastel-red-step-1"]
        hue_range: "24°–41°"
        lightness_range: "0.70–0.74"
        content_categories:
          - "contact and conversation"
          - "CTA hover shifts"
          - "team and people sections"
          - "customer stories and testimonials"
        usage:
          section_accent: "CTA accent color, hover state shifts, warm section tints"
          text: "Pastel red (L=0.70) works as text on dark backgrounds only"
          dark_mode_tint: "oklch(0.7 0.1909 24.11 / 10%) over neutral.950"

      results:
        id: "color.palette-semantics.results"
        type: "reference"
        description: "Optimism, value, proof. Warm-bright tones for metrics and outcomes."
        tokens: ["color.brand.pastel-red-step-2", "color.brand.pastel-red-step-3", "color.brand.cyber-yellow"]
        hue_range: "58°–92°"
        lightness_range: "0.79–0.87"
        content_categories:
          - "metrics, stats, and proof points"
          - "financial outcomes and ROI"
          - "stranded capacity recovery"
          - "positive performance indicators"
        usage:
          section_accent: "Metric highlights, stat number accents, subtle warm backgrounds"
          text: "HIGH CONTRAST RISK — these are very light (L=0.79-0.87). Only use on dark backgrounds."
          dark_mode_tint: "oklch(0.87 0.1786 92.23 / 8%) over neutral.950 — visible due to high lightness"

      growth:
        id: "color.palette-semantics.growth"
        type: "reference"
        description: "Sustainability, recovery, completion. Green-teal tones closing the loop back to brand."
        tokens: ["color.brand.cyber-yellow-step-1", "color.brand.cyber-yellow-step-2", "color.brand.cyber-yellow-step-3"]
        hue_range: "113°–164°"
        lightness_range: "0.75–0.81"
        content_categories:
          - "M&V (measurement and verification) results"
          - "capacity recovery outcomes"
          - "sustainability and environmental impact"
          - "completion and success states"
        usage:
          section_accent: "Background tints for outcome/success sections"
          text: "Light (L=0.75-0.81) — dark backgrounds only"
          dark_mode_tint: "oklch(0.7698 0.1588 137.1 / 10%) over neutral.950"

      neutral:
        id: "color.palette-semantics.neutral"
        type: "reference"
        description: "Breathing room, readability, structural clarity. Use between colored sections to prevent chromatic fatigue."
        tokens: ["color.neutral.50", "color.neutral.100", "color.neutral.200", "color.neutral.800", "color.neutral.900", "color.neutral.950"]
        content_categories:
          - "body text and long-form content"
          - "structural sections (purpose-built, deploy at scale)"
          - "breathing room between colored sections"
          - "form and input backgrounds"
        usage:
          section_accent: "Default section backgrounds — white/neutral.100 (light), neutral.950/900 (dark)"
          text: "Primary text color in both modes"

    # --- Taste boundaries (upper AND lower) ---
    #
    # Brand pillars: Precise, Masterful, Refined, Pioneering.
    # "Credibility through restraint. Sophisticated buyers, not impulse shoppers."
    # "Controlled color palette, no visual clutter." (Masterful)
    # "Subtle use of the brand gradient." (Refined)
    # "Every element earns its place." (Precision)
    #
    # These rules codify restraint. Without ceilings, an AI agent will
    # optimize toward maximum palette coverage — which reads as a rainbow.
    # Color is earned, not assigned by lookup table.
    #
    taste_boundaries:
      teal_dominance:
        id: "color.palette-semantics.teal-dominance"
        severity: "error"
        type: "constraint"
        description: "Teal (trust region) + neutrals must comprise at least 70% of any surface's color expression. The brand IS teal — other palette regions are rare accents, not co-primaries. 'Controlled color palette, no visual clutter.' (Masterful pillar)"
        test:
          min: 0.7
      max_regions_per_surface:
        id: "color.palette-semantics.max-regions"
        severity: "error"
        type: "constraint"
        description: "Maximum non-neutral palette regions per surface. 'Subtle use of the brand gradient.' (Refined pillar) — every accent must earn its place."
        web_page: 2           # Homepage/landing: trust + 1 accent (2 accents for long pages with 10+ sections, requires justification)
        landing_page: 1       # Focused landing pages: trust only (+ neutrals)
        ad_banner: 1          # Ads: brand teal only (+ neutrals)
        email: 1              # Emails: trust only (+ neutrals)
        white_paper: 2        # Documents: trust + 1 accent for data sections
        physical_goods: 1     # Hardware/packaging: brand teal only
        data_visualization: 6 # Charts: full palette allowed (this is what the gradient was designed for)
        slide_deck: 2         # Presentations: trust + 1 accent per slide
      max_colored_sections:
        id: "color.palette-semantics.max-colored-sections"
        severity: "warning"
        type: "constraint"
        description: "On a multi-section page, no more than 30% of sections should have non-neutral palette accents. 'Every element earns its place.' (Precision pillar)"
        test:
          max: 0.3

    # --- Section color rules (evaluator-enforceable) ---
    rules:
      - id: "color.palette-semantics.no-monotone"
        description: "Homepage should use at least 1 palette region beyond teal + neutrals for visual interest (but no more than 2 total non-neutral regions — see max-regions)"
        severity: "warning"
        type: "constraint"
        test:
          min: 1
          max: 2
      - id: "color.palette-semantics.no-random-color"
        description: "Section accent colors must match the content category — do not assign palette regions arbitrarily"
        severity: "error"
        type: "constraint"
      - id: "color.palette-semantics.neutral-breathing-room"
        description: "At least 70% of page sections should use neutral backgrounds. Color is the exception, not the default. 'Credibility through restraint.'"
        severity: "warning"
        type: "constraint"
        test:
          min: 0.7
      - id: "color.palette-semantics.adjacent-colored-sections"
        description: "Never place two non-neutral colored sections adjacent to each other. Always separate with a neutral section."
        severity: "error"
        type: "constraint"
      - id: "color.palette-semantics.dark-tint-opacity"
        description: "Dark mode brand tints must use 15%+ opacity for depth region, 10%+ for others — below this they are imperceptible"
        severity: "error"
        type: "constraint"
      - id: "color.palette-semantics.tint-contrast"
        description: "Text on brand-tinted dark backgrounds must still pass WCAG AA (4.5:1). Verify contrast after applying tint."
        severity: "error"
        type: "constraint"
        test:
          min: 4.5
      - id: "color.palette-semantics.accent-subtlety"
        description: "Section palette accents should be subtle — background tints and label colors, NOT full-saturation section backgrounds. The palette region informs a hint, not a paint job."
        severity: "error"
        type: "constraint"

  # ---------------------------------------------------------------------------
  # Section flow rules — lightness rhythm and transition discipline
  #
  # Pages have a lightness direction. Alternating dark/light sections
  # creates a strobe effect. Commit to a direction and vary within it;
  # reserve dark moments for deliberate emphasis.
  # ---------------------------------------------------------------------------
  section_flow:
    id: "color.section-flow"
    type: "reference"
    description: "Rules governing how section background lightness changes as the user scrolls. Prevents dark/light strobing and creates cohesive page rhythm."

    lightness_direction:
      id: "color.section-flow.lightness-direction"
      severity: "error"
      type: "constraint"
      description: "Commit to a lightness direction within content sections. A light-mode page flows through light backgrounds (white ↔ neutral.100 ↔ neutral.50). A dark-mode page flows through dark backgrounds (neutral.950 ↔ neutral.900 ↔ neutral.800). Do not alternate between dark and light sections within the same content flow."

    tonal_variation:
      id: "color.section-flow.tonal-variation"
      type: "reference"
      description: "Differentiate adjacent neutral sections with subtle tonal shifts, not lightness jumps."
      light_mode:
        backgrounds: ["white", "oklch(0.985 0 0)", "oklch(0.967 0.001 286.375)"]  # white, neutral.50, neutral.100
        description: "Alternate between white and neutral.100 for gentle rhythm. Never jump to neutral.800+ within a light flow."
      dark_mode:
        backgrounds: ["oklch(0.141 0.005 285.823)", "oklch(0.21 0.006 285.885)", "oklch(0.274 0.006 286.033)"]  # neutral.950, neutral.900, neutral.800
        description: "Alternate between neutral.950 and neutral.900 for gentle rhythm. Never jump to white within a dark flow."

    dark_accent_moment:
      id: "color.section-flow.dark-accent-moment"
      severity: "error"
      type: "constraint"
      description: "A light-mode page may include at most ONE dark-background section as a deliberate accent moment (e.g., a hero, a detection callout, or a CTA strip). This is the only justified lightness inversion. More than one dark section in a light page creates strobing."
      test:
        max: 1
      exceptions:
        - "Hero section (always dark) does not count toward the limit — it precedes the content flow"
        - "Footer (always dark) does not count — it closes the page"

    hero_transition:
      id: "color.section-flow.hero-transition"
      severity: "warning"
      type: "constraint"
      description: "If the hero is dark and the content flow is light, the transition should happen once at the hero boundary. Do not return to dark until the accent moment (if any)."

    rules:
      - id: "color.section-flow.no-strobe"
        description: "Never alternate dark/light/dark/light across consecutive sections. If three adjacent sections have backgrounds with OKLch lightness values that alternate high/low/high or low/high/low, this is a strobe pattern."
        severity: "error"
        type: "pattern"
        test:
          description: "Check that consecutive section backgrounds do not alternate between L>0.8 and L<0.3"
      - id: "color.section-flow.max-lightness-jump"
        description: "Adjacent sections should not have a lightness difference greater than 0.5 OKLch L*, except at the hero boundary and footer boundary. Within the content flow, keep jumps under 0.15 L*."
        severity: "warning"
        type: "constraint"
        test:
          content_flow_max_delta: 0.15
          boundary_exception: ["hero", "footer"]
      - id: "color.section-flow.consistent-text-color"
        description: "Within a content flow, text color should stay consistent. Light flow = dark text throughout. Dark flow = light text throughout. Do not force the reader to re-adapt between sections."
        severity: "warning"
        type: "constraint"


# =============================================================================
# TYPOGRAPHY
# =============================================================================
typography:
  # ---------------------------------------------------------------------------
  # Font stacks — enforceable via CSS audit
  # ---------------------------------------------------------------------------
  font_stacks:
    id: "typography.font-stacks"
    type: "value"
    severity: "error"
    description: "Required font families — flag if other fonts appear in source"
    body:
      family: "Inter"
      fallback: "Inter, ui-sans-serif, system-ui, -apple-system, sans-serif"
      usage: "All body text, UI labels, navigation"
    display:
      family: "Lato"
      fallback: "Lato, ui-sans-serif, system-ui, -apple-system, sans-serif"
      usage: "www headings only; Patina uses Inter for headings"
      scope: "www"
    mono:
      family: "JetBrains Mono"
      fallback: "JetBrains Mono, ui-monospace, SFMono-Regular, Menlo, monospace"
      usage: "Code blocks, technical values, data labels"

  # ---------------------------------------------------------------------------
  # Heading sizes — desktop AND mobile breakpoints
  # Source of truth: tokens/typography/scale.json
  # ---------------------------------------------------------------------------
  headings:
    h1:
      desktop:
        size: "4rem"       # 64px
        weight: 700
        line_height: 1.1
        letter_spacing: "-0.02em"
      mobile:
        size: "2.75rem"    # 44px
        weight: 700
        line_height: 1.1
        letter_spacing: "-0.02em"
      breakpoint: "1024px"  # Canonical; www uses 991px (should migrate)
    h2:
      desktop:
        size: "3rem"       # 48px
        weight: 700
        line_height: 1.2
        letter_spacing: "-0.02em"
      mobile:
        size: "2rem"       # 32px
        weight: 700
        line_height: 1.2
        letter_spacing: "-0.02em"
      breakpoint: "1024px"
    h3:
      desktop:
        size: "2rem"       # 32px
        weight: 700
        line_height: 1.3
        letter_spacing: "0"
      mobile:
        size: "1.5rem"     # 24px
        weight: 700
        line_height: 1.3
        letter_spacing: "0"
      breakpoint: "1024px"

  # ---------------------------------------------------------------------------
  # Body sizes
  # ---------------------------------------------------------------------------
  body:
    large:
      size: "1.25rem"      # 20px
      line_height: 1.6
      letter_spacing: "0"
    medium:
      size: "1.125rem"     # 18px
      line_height: 1.6
      letter_spacing: "0"
    regular:
      size: "1rem"         # 16px
      line_height: 1.6
      letter_spacing: "0"
    small:
      size: "0.875rem"     # 14px
      line_height: 1.6
      letter_spacing: "0"

  # ---------------------------------------------------------------------------
  # Font weight constraints per element type
  # ---------------------------------------------------------------------------
  weight_rules:
    - id: "typography.weight.headings"
      severity: "error"
      type: "constraint"
      element: "h1, h2, h3"
      allowed_weights: [700]
      description: "Headings must be bold (700). brand_rules.yml says 600 for H2 — WRONG, CSS is truth."
      test:
        values: [700]
    - id: "typography.weight.subheadings"
      severity: "error"
      type: "constraint"
      element: "h4, h5, h6"
      allowed_weights: [600, 700]
      description: "Subheadings: semibold or bold"
      test:
        values: [600, 700]
    - id: "typography.weight.body"
      severity: "error"
      type: "constraint"
      element: "body, p, li, td"
      allowed_weights: [400]
      description: "Body text: regular weight only"
      test:
        values: [400]
    - id: "typography.weight.navigation"
      severity: "warning"
      type: "constraint"
      element: "a, nav, label"
      allowed_weights: [400, 500]
      description: "Navigation and labels: regular or medium"
      test:
        values: [400, 500]
    - id: "typography.weight.buttons"
      severity: "error"
      type: "constraint"
      element: "button, .cta"
      allowed_weights: [600]
      description: "Buttons and CTAs: semibold. Exception: .cta-link uses 500 (see components.cta-link)."
      test:
        values: [600]
    - id: "typography.weight.emphasis"
      severity: "warning"
      type: "constraint"
      element: "strong, b"
      allowed_weights: [600, 700]
      description: "Emphasis: semibold or bold"
      test:
        values: [600, 700]

  # ---------------------------------------------------------------------------
  # Line height requirements
  # ---------------------------------------------------------------------------
  line_height_rules:
    - id: "typography.line-height.headings"
      severity: "error"
      type: "constraint"
      context: "headings"
      named_scale: "tight"
      min: 1.1
      max: 1.3
      description: "Headings: tight leading (1.1 for h1, 1.2 for h2, 1.3 for h3)"
      test:
        min: 1.1
        max: 1.3
    - id: "typography.line-height.body"
      severity: "error"
      type: "constraint"
      context: "body"
      named_scale: "relaxed"
      value: 1.6
      description: "All body text uses 1.6 line-height"
      test:
        value: 1.6
    - id: "typography.line-height.ui-compact"
      severity: "warning"
      type: "constraint"
      context: "ui_compact"
      min: 1.2
      max: 1.4
      description: "Compact UI elements (badges, pills, table cells)"
      test:
        min: 1.2
        max: 1.4

  # ---------------------------------------------------------------------------
  # Letter spacing rules
  # ---------------------------------------------------------------------------
  letter_spacing_rules:
    - id: "typography.letter-spacing.large-headings"
      severity: "warning"
      type: "value"
      context: "h1, h2"
      value: "-0.02em"
      description: "Large headings get slight tightening"
      test:
        value: "-0.02em"
    - id: "typography.letter-spacing.small-headings"
      severity: "warning"
      type: "value"
      context: "h3, h4, h5, h6"
      value: "0"
      description: "Smaller headings: no adjustment"
      test:
        value: "0"
    - id: "typography.letter-spacing.body"
      severity: "warning"
      type: "value"
      context: "body"
      value: "0"
      description: "Body text: no adjustment"
      test:
        value: "0"
    - id: "typography.letter-spacing.uppercase"
      severity: "warning"
      type: "value"
      context: "uppercase"
      value: "0.05em"
      description: "Uppercase text (labels, overlines) needs loose tracking"
      test:
        value: "0.05em"


# =============================================================================
# SPACING
# =============================================================================
spacing:
  # ---------------------------------------------------------------------------
  # 4px grid compliance
  # ---------------------------------------------------------------------------
  grid:
    base_unit: "4px"
    base_rem: "0.25rem"
    description: "All spacing values must be multiples of 4px"
    allowed_scale:
      - { token: "0", value: "0" }
      - { token: "1", value: "0.25rem" }    #  4px
      - { token: "2", value: "0.5rem" }     #  8px
      - { token: "3", value: "0.75rem" }    # 12px
      - { token: "4", value: "1rem" }       # 16px
      - { token: "5", value: "1.25rem" }    # 20px
      - { token: "6", value: "1.5rem" }     # 24px
      - { token: "8", value: "2rem" }       # 32px
      - { token: "10", value: "2.5rem" }    # 40px
      - { token: "12", value: "3rem" }      # 48px
      - { token: "16", value: "4rem" }      # 64px
      - { token: "20", value: "5rem" }      # 80px
      - { token: "24", value: "6rem" }      # 96px
      - { token: "32", value: "8rem" }      # 128px
    enforce: true
    id: "spacing.grid"
    severity: "error"
    type: "constraint"
    description: "All spacing values must be multiples of 4px"
    exceptions:
      - "1px"       # borders
      - "0.375rem"  # 6px button radius (legacy)
      - "0.625rem"  # 10px canonical radius
      - "9999px"    # pill radius

  # ---------------------------------------------------------------------------
  # Section padding — desktop and mobile
  # ---------------------------------------------------------------------------
  section_padding:
    standard:
      desktop: "4rem"       # 64px vertical
      mobile: "4rem"        # Same — standard sections don't shrink
    large:
      desktop: "8rem"       # 128px — hero/feature sections
      mobile: "5rem"        # 80px
    medium:
      desktop: "5rem"       # 80px — secondary sections
      mobile: "5rem"

  # ---------------------------------------------------------------------------
  # Page padding (horizontal)
  # ---------------------------------------------------------------------------
  page_padding:
    desktop: "2.5rem"       # 40px
    mobile: "1.5rem"        # 24px
    breakpoint: "1024px"    # Canonical; www uses 991px (should migrate)

  # ---------------------------------------------------------------------------
  # Container
  # ---------------------------------------------------------------------------
  container:
    max_width: "80rem"      # 1280px
    description: "Both www (.container-medium) and Patina use this max-width"

  # ---------------------------------------------------------------------------
  # Responsive hero heights
  # ---------------------------------------------------------------------------
  hero_height:
    base: "22rem"           # 352px — mobile
    sm: "25rem"             # 400px at 640px+
    md: "31.25rem"          # 500px at 768px+
    breakpoints:
      sm: "640px"
      md: "768px"


# =============================================================================
# MOTION
# =============================================================================
motion:
  # ---------------------------------------------------------------------------
  # Duration tokens
  # ---------------------------------------------------------------------------
  durations:
    fast: "150ms"           # Micro-interactions (hover feedback)
    normal: "200ms"         # Button hover transitions
    moderate: "300ms"       # Card hover, image zoom
    slow: "500ms"           # Hero animations, slide-up
    spin: "800ms"           # Spinner rotation

  # ---------------------------------------------------------------------------
  # Easing tokens
  # ---------------------------------------------------------------------------
  easings:
    default: "ease"         # General-purpose
    out: "ease-out"         # Entrance animations
    linear: "linear"        # Continuous rotation only

  # ---------------------------------------------------------------------------
  # Named animation patterns — required CSS for each
  # ---------------------------------------------------------------------------
  patterns:
    hover-lift:
      description: "Card/element lifts on hover with shadow"
      duration: "500ms"
      easing: "ease"
      css: "transition: transform 500ms ease, box-shadow 500ms ease"
      transform: "translateY(-4px)"
      requires_hover_query: true
    slide-up:
      description: "Element slides up into view on scroll/mount"
      duration: "500ms"
      easing: "ease-out"
      css: "transition: transform 500ms ease-out, opacity 500ms ease-out"
      transform_from: "translateY(20px)"
      transform_to: "translateY(0)"
      opacity_from: 0
      opacity_to: 1
    image-zoom:
      description: "Image scales slightly on hover"
      duration: "300ms"
      easing: "ease"
      css: "transition: transform 300ms ease"
      transform: "scale(1.05)"
      requires_hover_query: true
    spinner:
      description: "Loading spinner continuous rotation"
      duration: "800ms"
      easing: "linear"
      css: "animation: spin 800ms linear infinite"
      keyframes: "from { transform: rotate(0deg) } to { transform: rotate(360deg) }"
    logo-marquee:
      id: "motion.patterns.logo-marquee"
      severity: "warning"
      type: "pattern"
      description: "CSS keyframe marquee for trust bar logos. Continuous horizontal scroll. Must respect reduced-motion."
      duration: "30s"
      easing: "linear"
      css: "animation: marquee 30s linear infinite"
      keyframes: "from { transform: translateX(0) } to { transform: translateX(-50%) }"
      requires_reduced_motion: true
      requires_duplicate_set: true  # Logo set must be duplicated for seamless loop
      test:
        regex: "animation:\\s*marquee|@keyframes\\s+marquee"

  # ---------------------------------------------------------------------------
  # Max duration constraint
  # ---------------------------------------------------------------------------
  max_interaction_duration:
    id: "motion.max-duration"
    severity: "error"
    type: "constraint"
    value: "500ms"
    description: "User-triggered transitions (hover, click) must not exceed this"
    test:
      max: 500

  # ---------------------------------------------------------------------------
  # Reduced motion — REQUIRED
  # ---------------------------------------------------------------------------
  reduced_motion:
    id: "motion.reduced-motion"
    severity: "error"
    type: "pattern"
    required: true
    description: "All animations MUST respect prefers-reduced-motion"
    media_query: "@media (prefers-reduced-motion: reduce)"
    fallback_patterns:
      - pattern: "transition"
        replacement: "transition-duration: 0.01ms !important"
        description: "Collapse transition durations"
      - pattern: "animation"
        replacement: "animation-duration: 0.01ms !important; animation-iteration-count: 1 !important"
        description: "Collapse animation durations, stop infinite loops"
      - pattern: "transform"
        replacement: "transform: none !important"
        description: "Remove transforms"
        severity: "warning"
    global_css: |
      @media (prefers-reduced-motion: reduce) {
        *, *::before, *::after {
          animation-duration: 0.01ms !important;
          animation-iteration-count: 1 !important;
          transition-duration: 0.01ms !important;
          scroll-behavior: auto !important;
        }
      }

  # ---------------------------------------------------------------------------
  # Hover media query — REQUIRED for hover effects
  # ---------------------------------------------------------------------------
  hover_gate:
    id: "motion.hover-gate"
    severity: "error"
    type: "pattern"
    required: true
    media_query: "@media (hover: hover) and (pointer: fine)"
    description: "All :hover effects must be wrapped in this media query to prevent sticky hover on touch devices"
    test:
      regex: ":hover(?![\\s\\S]*@media\\s*\\(hover:\\s*hover\\))"
    example_css: |
      @media (hover: hover) and (pointer: fine) {
        .card:hover {
          transform: translateY(-4px);
        }
      }


# =============================================================================
# COMPONENTS
# =============================================================================
components:
  # ---------------------------------------------------------------------------
  # Border radius scale
  # ---------------------------------------------------------------------------
  radius:
    base:
      value: "0.625rem"    # 10px — Patina canonical
      description: "Primary radius. www should migrate from 0.5rem."
    sm:
      value: "calc(0.625rem - 4px)"  # ~6px
      description: "Tight radius — inner elements, badges"
    md:
      value: "calc(0.625rem - 2px)"  # ~8px
      description: "Medium radius — inputs, small cards"
    lg:
      value: "0.625rem"    # 10px — same as base
      description: "Large radius — cards, dialogs"
    xl:
      value: "calc(0.625rem + 4px)"  # ~14px
      description: "Extra large — modals, prominent surfaces"
    full:
      value: "9999px"
      description: "Pill shape — tags, badges, avatars"
    button:
      value: "0.375rem"    # 6px — www legacy
      description: "Button radius (www). Deviation from Patina's shadcn default."
      migration_target: "sm"
    www_legacy:
      value: "0.5rem"      # 8px
      description: "www's current --radius. Should migrate to base (0.625rem)."
      deprecated: true

  # ---------------------------------------------------------------------------
  # Focus indicator requirements
  # ---------------------------------------------------------------------------
  focus:
    id: "components.focus"
    type: "value"
    required: true
    style: "outline"
    outline_width: "2px"
    outline_style: "solid"
    outline_offset: "2px"
    outline_color:
      light: "oklch(0.705 0.015 286.067)"   # neutral.400
      dark: "oklch(0.552 0.016 285.938)"     # neutral.500
      www_override: "oklch(0.55 0.12 160)"   # www-ring (green-shifted, should migrate)
    css: "outline: 2px solid var(--ring); outline-offset: 2px"
    rules:
      - id: "components.focus.no-outline-none"
        description: "Never use outline: none without a visible alternative"
        type: "pattern"
        pattern: "outline:\\s*none"
        severity: "error"
        exception: "Only if replaced by box-shadow or border focus indicator"
        test:
          regex: "outline:\\s*none"
      - id: "components.focus.visible-both-modes"
        description: "Focus indicators must be visible in both light and dark modes"
        type: "constraint"
        severity: "error"
      - id: "components.focus.not-color-only"
        description: "Focus must not rely solely on color change"
        type: "constraint"
        severity: "error"

  # ---------------------------------------------------------------------------
  # Button styling rules
  # ---------------------------------------------------------------------------
  buttons:
    primary:
      background: "oklch(0.21 0.006 285.885)"    # neutral.900
      text_color: "oklch(0.985 0 0)"              # neutral.50
      hover_background: "oklch(0.21 0.006 285.885 / 0.9)"
      border_radius: "0.375rem"                    # www button radius
      font_weight: 600
      min_height: "2.5rem"                         # 40px — touch target
      min_width: "6rem"
      padding: "0.5rem 1rem"
    secondary:
      background: "transparent"
      text_color: "oklch(0.21 0.006 285.885)"     # neutral.900
      border: "1px solid oklch(0.92 0.004 286.32)" # neutral.200
      hover_background: "oklch(0.967 0.001 286.375)" # neutral.100
      border_radius: "0.375rem"
      font_weight: 600
      min_height: "2.5rem"
      padding: "0.5rem 1rem"
    ghost_on_dark:
      id: "components.buttons.ghost-on-dark"
      severity: "error"
      type: "value"
      description: "Ghost button for dark backgrounds — transparent with white border. Used for secondary hero CTAs. Validated across 7/12 B2B infra competitor audits."
      context: "dark_background"
      background: "transparent"
      text_color: "oklch(1 0 0)"                    # white
      border: "1px solid oklch(1 0 0 / 0.6)"        # white/60
      hover_background: "oklch(1 0 0 / 0.1)"        # white/10
      border_radius: "0.375rem"                      # www button radius
      font_weight: 600
      min_height: "2.5rem"                           # 40px — touch target
      padding: "0.5rem 1rem"
      requires_hover_query: true
      test:
        regex: "border-white\\/60|border-color:\\s*oklch\\(1\\s+0\\s+0\\s*\\/\\s*0?\\.?6"
    rules:
      - id: "components.buttons.touch-target"
        description: "Buttons must have min 44px touch target (WCAG 2.5.8)"
        type: "constraint"
        min_touch_target: "44px"
        severity: "warning"
        test:
          min: 44
      - id: "components.buttons.min-weight"
        description: "Button text must not use font-weight below 600"
        type: "constraint"
        severity: "error"
        test:
          min: 600
      - id: "components.buttons.disabled-state"
        description: "Disabled buttons must have reduced opacity (0.5) and cursor: not-allowed"
        type: "pattern"
        severity: "error"

  # ---------------------------------------------------------------------------
  # Dark mode token swap rules
  # ---------------------------------------------------------------------------
  dark_mode_tokens:
    description: "Required token swaps when .dark class or prefers-color-scheme: dark"
    swaps:
      - token: "background"
        light: "oklch(1 0 0)"                     # white
        dark: "oklch(0.141 0.005 285.823)"         # neutral.950
      - token: "foreground"
        light: "oklch(0.141 0.005 285.823)"        # neutral.950
        dark: "oklch(0.985 0 0)"                   # neutral.50
      - token: "card"
        light: "oklch(1 0 0)"                      # white
        dark: "oklch(0.21 0.006 285.885)"          # neutral.900
      - token: "muted"
        light: "oklch(0.967 0.001 286.375)"        # neutral.100
        dark: "oklch(0.274 0.006 286.033)"         # neutral.800
      - token: "muted-foreground"
        light: "oklch(0.552 0.016 285.938)"        # neutral.500
        dark: "oklch(0.705 0.015 286.067)"         # neutral.400
      - token: "border"
        light: "oklch(0.92 0.004 286.32)"          # neutral.200
        dark: "oklch(0.274 0.006 286.033)"         # neutral.800
      - token: "ring"
        light: "oklch(0.705 0.015 286.067)"        # neutral.400
        dark: "oklch(0.552 0.016 285.938)"         # neutral.500
      - token: "destructive"
        light: "oklch(0.577 0.245 27.325)"
        dark: "oklch(0.704 0.191 22.216)"
    rules:
      - id: "components.dark-mode.light-dark-pair"
        description: "Every semantic color token must have both light and dark values"
        type: "constraint"
        severity: "error"
      - id: "components.dark-mode.no-pure-black"
        description: "Dark mode must not use pure black (#000000) as background"
        type: "pattern"
        severity: "error"
        test:
          regex: "\\.dark[^}]*background[^;]*#000000|background[^;]*#000(?:;|\\s)"
      - id: "components.dark-mode.translucent-borders"
        description: "Dark mode borders should use semi-transparent values for depth"
        type: "pattern"
        severity: "warning"
      - id: "components.dark-mode.code-block-bg"
        description: "Code blocks (pre) must have distinct background from page in dark mode — use neutral.800 not neutral.950"
        type: "value"
        severity: "error"
      - id: "components.dark-mode.link-distinction"
        description: "Links must be visually distinguishable from body text in dark mode — use brand teal or underline"
        type: "constraint"
        severity: "error"
      - id: "components.dark-mode.persist-preference"
        description: "Dark mode preference must persist across page navigation via localStorage"
        type: "pattern"
        severity: "error"
      - id: "components.dark-mode.head-init"
        description: "Dark mode init must run before first paint (in <head>, not <body>) to prevent FOUC"
        type: "pattern"
        severity: "error"

  # ---------------------------------------------------------------------------
  # CTA link pattern (homepage overhaul — Z2O-995)
  # ---------------------------------------------------------------------------
  cta_link:
    id: "components.cta-link"
    severity: "warning"
    type: "value"
    description: "CTA inline link — medium weight (not semibold), with hover-gated underline animation. Typographically lighter than buttons. Distinct from button link variant."
    font_weight: 500                                # Intentional: 500 not 600 — CTA links are lighter than buttons
    text_decoration: "none"
    hover_text_decoration: "underline"
    hover_underline_offset: "4px"
    requires_hover_query: true
    css: "font-medium no-underline hover:underline underline-offset-4"
    test:
      regex: "cta-link|font-medium[^;]*hover:.*underline"

  # ---------------------------------------------------------------------------
  # Stats row pattern (homepage overhaul — Z2O-995)
  # ---------------------------------------------------------------------------
  stats_row:
    id: "components.stats-row"
    severity: "warning"
    type: "value"
    description: "Stats display row — large numbers in primary color, uppercase labels with tracking. Used for trust/metric bars on homepage and landing pages."
    number:
      color: "var(--color-semantic-primary)"
      font_weight: 700
      min_size: "2rem"                              # 32px — large enough for visual impact
    label:
      text_transform: "uppercase"
      letter_spacing: "0.05em"                      # Matches typography.letter-spacing.uppercase rule
      font_weight: 500
      color: "var(--color-semantic-muted-foreground)"
    layout: "horizontal"
    responsive: "stack vertical below sm (640px)"

  # ---------------------------------------------------------------------------
  # Logo marquee bar (homepage overhaul — Z2O-995)
  # ---------------------------------------------------------------------------
  logo_marquee_bar:
    id: "components.logo-marquee-bar"
    severity: "warning"
    type: "reference"
    description: "Trust bar with scrolling logos. Requires overflow-hidden container, duplicated logo set for seamless loop. Animation defined in motion.patterns.logo-marquee."
    overflow: "hidden"
    logo_max_height: "2rem"                         # 32px — consistent sizing
    logo_opacity: 0.7                               # Muted to avoid competing with content
    gap: "3rem"                                     # 48px between logos
    animation_ref: "motion.patterns.logo-marquee"
    requires_reduced_motion: true                   # Falls back to static display

  # ---------------------------------------------------------------------------
  # Dark image container (homepage overhaul — Z2O-995)
  # ---------------------------------------------------------------------------
  dark_image_container:
    id: "components.dark-image-container"
    severity: "warning"
    type: "value"
    description: "Dark wrapper (bg-neutral-900) for showcasing dark-themed screenshots/images on light pages. Prevents jarring contrast between light page and dark UI screenshots."
    background: "oklch(0.21 0.006 285.885)"         # neutral.900
    border_radius: "0.625rem"                       # base radius
    padding: "2rem"                                 # 32px internal padding
    overflow: "hidden"

  # ---------------------------------------------------------------------------
  # CTA strip cards (homepage overhaul — Z2O-995)
  # ---------------------------------------------------------------------------
  cta_strip_cards:
    id: "components.cta-strip-cards"
    severity: "warning"
    type: "reference"
    description: "Multi-card horizontal strip with hover-lift. Each card is a CTA with heading, description, and optional icon. Uses motion.patterns.hover-lift for hover state."
    layout: "horizontal"
    gap: "1.5rem"                                   # 24px
    card_min_width: "16rem"                         # 256px
    card_padding: "1.5rem"                          # 24px
    card_border_radius: "0.625rem"                  # base radius
    hover_effect_ref: "motion.patterns.hover-lift"
    requires_hover_query: true
    responsive: "scroll horizontal on mobile, grid on desktop"


# =============================================================================
# BREAKPOINTS
# =============================================================================
breakpoints:
  # ---------------------------------------------------------------------------
  # Standard breakpoints (Tailwind defaults)
  # ---------------------------------------------------------------------------
  standard:
    sm:
      value: "640px"
      rem: "40rem"
      description: "Small — landscape phones, small tablets"
    md:
      value: "768px"
      rem: "48rem"
      description: "Medium — tablets"
    lg:
      value: "1024px"
      rem: "64rem"
      description: "Large — small laptops, landscape tablets"
    xl:
      value: "1280px"
      rem: "80rem"
      description: "Extra large — desktops"
    "2xl":
      value: "1536px"
      rem: "96rem"
      description: "2X large — large desktops"

  # ---------------------------------------------------------------------------
  # Patina custom breakpoints (ultra-wide displays)
  # ---------------------------------------------------------------------------
  patina_custom:
    "3xl":
      value: "1792px"
      rem: "112rem"
      description: "Patina custom — wide monitors"
    "4xl":
      value: "2048px"
      rem: "128rem"
      description: "Patina custom — 2K displays"
    "5xl":
      value: "2304px"
      rem: "144rem"
      description: "Patina custom — ultrawide"
    "6xl":
      value: "2560px"
      rem: "160rem"
      description: "Patina custom — QHD"
    "7xl":
      value: "2816px"
      rem: "176rem"
      description: "Patina custom — large ultrawide"
    "8xl":
      value: "3072px"
      rem: "192rem"
      description: "Patina custom — 4K and beyond"

  # ---------------------------------------------------------------------------
  # Migration notes
  # ---------------------------------------------------------------------------
  migration:
    www_tablet:
      id: "breakpoints.migration.www-tablet"
      type: "value"
      current: "991px"
      target: "1024px"
      target_token: "lg"
      description: "www uses 991px for heading/padding responsive breakpoint. Should migrate to standard lg (1024px)."
      severity: "warning"
      affected:
        - "heading responsive sizes (h1, h2, h3)"
        - "padding-global responsive switch"
        - "section-pad-large responsive switch"

  # ---------------------------------------------------------------------------
  # Enforcement
  # ---------------------------------------------------------------------------
  rules:
    - id: "breakpoints.custom-banned"
      description: "Custom breakpoints outside the standard + patina set are flagged"
      type: "pattern"
      severity: "warning"
    - id: "breakpoints.mobile-first"
      description: "Mobile-first approach: use min-width media queries"
      type: "pattern"
      severity: "warning"
      test:
        regex: "max-width:\\s*\\d+px"


# =============================================================================
# ELEVATION
# =============================================================================
elevation:
  # ---------------------------------------------------------------------------
  # Shadow scale — only these tokens are allowed
  # Source: tokens/elevation/shadow.json (Tailwind v4 defaults from Patina)
  # ---------------------------------------------------------------------------
  shadows:
    allowed_tokens:
      none:
        value: "none"
        usage: "Reset/removal — sidebar-header, input-group, toggle-group flush"
      xs:
        value: "0 1px 2px 0 rgb(0 0 0 / 0.05)"
        usage: "Form controls — input, textarea, select, checkbox, switch, button (outline)"
      sm:
        value: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)"
        usage: "Containers — card, slider thumb, tabs (active), sidebar (floating)"
      md:
        value: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
        usage: "Floating elements — dropdown, select content, popover, hover-card"
      lg:
        value: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
        usage: "Overlays — dialog, alert-dialog, sheet, dropdown sub-content"
      xl:
        value: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"
        usage: "Maximum elevation — chart tooltips"
    rules:
      - id: "elevation.shadows.no-adhoc"
        description: "Ad-hoc box-shadow values are banned — use shadow tokens (none/xs/sm/md/lg/xl)"
        type: "pattern"
        severity: "error"
        detection: "box-shadow values not matching any allowed_tokens value"
        exception: "sidebar-border shadow (outline-as-shadow pattern)"
      - id: "elevation.shadows.context-match"
        description: "Shadow elevation must match component context (e.g., cards use sm, modals use lg)"
        type: "constraint"
        severity: "warning"
      - id: "elevation.shadows.hover-transition"
        description: "Hover states that add shadow must transition from a lower token, not from none to lg"
        type: "constraint"
        severity: "warning"

  # ---------------------------------------------------------------------------
  # Z-index layers — strict layer system
  # Source: tokens/elevation/z-index.json (Tailwind v4 defaults from Patina)
  # ---------------------------------------------------------------------------
  z_index:
    allowed_layers:
      base:
        value: 0
        usage: "Default stacking context"
      scrollbar:
        value: 1
        usage: "Scroll-area scrollbar thumb"
      raised:
        value: 10
        usage: "Locally raised — sidebar panel, resize handle, focused group item"
      sticky:
        value: 20
        usage: "Sticky/resize affordances — sidebar resize handle"
      overlay:
        value: 50
        usage: "Top-level overlays — dialog, sheet, dropdown, popover, tooltip, header"
    rules:
      - id: "elevation.z-index.no-custom"
        description: "Custom z-index values outside the allowed layer set are banned"
        type: "pattern"
        severity: "error"
        detection: "z-index values not matching 0, 1, 10, 20, or 50"
        exception: "Negative z-index for stacking context tricks (z-[-1])"
        test:
          values: [0, 1, 10, 20, 50]
      - id: "elevation.z-index.max-50"
        description: "Z-index must not exceed 50 — no z-[999] or z-[9999] hacks"
        type: "constraint"
        severity: "error"
        test:
          max: 50
      - id: "elevation.z-index.overlay-backdrop"
        description: "Elements at z-overlay (50) must have a backdrop or dismiss mechanism"
        type: "constraint"
        severity: "warning"
      - id: "elevation.z-index.stacking-context"
        description: "Z-index should only be set on elements that create a stacking context"
        type: "constraint"
        severity: "warning"


# =============================================================================
# ACCESSIBILITY (retained from v0.1.0, cross-references color/components)
# =============================================================================
accessibility:
  require_alt_text: true
  require_aria_labels: true
  require_focus_indicators: true   # See components.focus for specifics
  min_contrast_ratio: 4.5          # See color.contrast for full breakdown
  touch_target_min: "44px"         # WCAG 2.5.8
  rules:
    - id: "accessibility.alt-text"
      description: "Images must have alt text (empty string for decorative)"
      type: "pattern"
      severity: "error"
      test:
        regex: "<img(?![^>]*alt=)"
    - id: "accessibility.aria-names"
      description: "Interactive elements must have accessible names"
      type: "pattern"
      severity: "error"
    - id: "accessibility.not-color-only"
      description: "Color must not be the sole means of conveying information"
      type: "constraint"
      severity: "error"
    - id: "accessibility.text-resize"
      description: "Text resizing to 200% must not break layout"
      type: "constraint"
      severity: "warning"
