{
  "$schema": "aihio-design-system",
  "version": "2.3.0",
  "intents": {
    "action": "Any user-triggerable action.",
    "primary-action": "The dominant action in a given context (e.g. submit, confirm).",
    "secondary-action": "A supporting action (e.g. cancel, reset).",
    "destructive-action": "An action with irreversible or data-losing consequences.",
    "toggle-state": "Toggles a binary state without navigating or submitting.",
    "navigation": "Moves the user to a different view, section, or URL.",
    "selection": "Lets the user pick one item from a set.",
    "form-field": "Collects a single value from the user as part of a form.",
    "text-entry": "A form field specifically for free-form text.",
    "status": "Displays current state (badges, pills, counts).",
    "informational": "Non-blocking guidance or context.",
    "alert": "Important message that requires attention.",
    "error": "Communicates a failure or invalid state.",
    "container": "Groups related content without imposing meaning.",
    "surface": "A raised or bordered region that frames content (card, panel).",
    "layout": "Arranges other components spatially (tab list, grid).",
    "header": "A titled heading region inside a surface.",
    "footer": "An actions or summary region inside a surface.",
    "overlay": "Content that renders on top of the page (dialog, popover).",
    "modal": "An overlay that blocks interaction with the rest of the page.",
    "menu": "A list of actions triggered from an anchor element.",
    "identity": "Represents a person or entity (avatar, name chip).",
    "labeling": "Short label or tag attached to other content.",
    "metadata": "Ancillary descriptive text (titles, descriptions, captions).",
    "tabs": "Switches between sibling panels using a row of triggers.",
    "tabular-data": "Records laid out in rows and columns, compared by scanning down a column (a data table).",
    "dismissible": "Can be closed or hidden by the user."
  },
  "components": [
    {
      "$component": "aihio-alert",
      "version": "1.1.0",
      "description": "A callout for a message the user should notice where it is shown: a failed save, a confirmation that something worked, a warning about a setting. Only the destructive variant interrupts a screen reader.",
      "intents": [
        "alert",
        "informational",
        "status"
      ],
      "attributes": {
        "variant": {
          "type": "enum",
          "values": [
            "default",
            "success",
            "warning",
            "destructive"
          ],
          "default": "default",
          "description": "Visual and live-region emphasis. destructive announces assertively; success, warning, and default announce politely."
        }
      },
      "slots": {
        "default": {
          "description": "Alert content"
        },
        "title": {
          "description": "Alert title (use slot=\"title\")"
        },
        "description": {
          "description": "Alert description (use slot=\"description\")"
        }
      },
      "composition": {
        "allowedSlots": [
          "default",
          "title",
          "description"
        ],
        "forbiddenChildren": [
          "aihio-button",
          "aihio-toggle",
          "aihio-switch",
          "aihio-input",
          "aihio-combobox",
          "aihio-dropdown",
          "aihio-dialog"
        ]
      },
      "a11yContract": {
        "handled": [
          "role=\"alert\" (assertive) is set for variant=\"destructive\"; other variants get role=\"status\" (polite) so a confirmation does not interrupt the screen reader",
          "An author-provided role is always left alone"
        ],
        "required": [
          {
            "when": "variant=\"destructive\"",
            "requirement": "Include slot=\"title\" or slot=\"description\" so assistive tech has content to announce. Colour alone is not a sufficient signal.",
            "severity": "warn",
            "rule": "alert-announced-content"
          },
          {
            "when": "role is set on the alert to anything but its variant's own (alert for destructive, status otherwise)",
            "requirement": "Remove the role and let the variant set it: role=\"alert\" for destructive, which interrupts the screen reader, and role=\"status\" for the rest, which waits its turn.",
            "severity": "error",
            "rule": "alert-role"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-alert variant=\"error\">Oops</aihio-alert>",
          "reason": "variant=\"error\" is not valid. Use variant=\"destructive\" for error states.",
          "fix": "<aihio-alert variant=\"destructive\">\n  <div slot=\"title\">Could not save</div>\n</aihio-alert>",
          "rule": "invalid-enum-attribute"
        },
        {
          "markup": "<aihio-alert><aihio-button>OK</aihio-button></aihio-alert>",
          "reason": "Alerts are non-interactive surfaces. For acknowledgeable prompts use aihio-dialog.",
          "fix": "<aihio-alert>\n  <div slot=\"title\">Your changes were saved</div>\n</aihio-alert>",
          "rule": "forbidden-descendant"
        },
        {
          "markup": "<aihio-alert variant=\"success\" role=\"alert\">Saved</aihio-alert>",
          "reason": "role=\"alert\" interrupts the screen reader. A confirmation should announce politely — drop the role and let the success variant set role=\"status\".",
          "fix": "<aihio-alert variant=\"success\">Saved</aihio-alert>",
          "rule": "alert-role"
        }
      ],
      "examples": [
        {
          "title": "Default",
          "description": "Neutral information, announced politely as role=\"status\".",
          "markup": "<aihio-alert>\n  <div slot=\"title\">Heads up!</div>\n  <div slot=\"description\">You can add components to your app.</div>\n</aihio-alert>"
        },
        {
          "title": "Destructive",
          "description": "A failure: the one variant announced assertively, as role=\"alert\".",
          "markup": "<aihio-alert variant=\"destructive\">\n  <div slot=\"title\">Error</div>\n  <div slot=\"description\">Something went wrong.</div>\n</aihio-alert>"
        },
        {
          "title": "Success",
          "description": "A confirmation that something worked.",
          "markup": "<aihio-alert variant=\"success\">\n  <div slot=\"title\">Changes saved</div>\n  <div slot=\"description\">Your profile is up to date.</div>\n</aihio-alert>"
        },
        {
          "title": "Warning",
          "description": "Something that needs attention but has not failed.",
          "markup": "<aihio-alert variant=\"warning\">\n  <div slot=\"title\">Trial ends in 3 days</div>\n  <div slot=\"description\">Add a payment method to keep your workspace active.</div>\n</aihio-alert>"
        }
      ]
    },
    {
      "$component": "aihio-avatar",
      "version": "1.0.0",
      "description": "A round picture of a person or entity, such as the account owner in a header, falling back to initials when there is no image or it fails to load.",
      "intents": [
        "identity",
        "labeling"
      ],
      "attributes": {
        "src": {
          "type": "string",
          "description": "Image URL for the avatar"
        },
        "alt": {
          "type": "string",
          "description": "Alt text for the image. Initials are derived from this if no fallback is set."
        },
        "fallback": {
          "type": "string",
          "description": "Fallback text displayed when image is unavailable"
        },
        "size": {
          "type": "enum",
          "values": [
            "sm",
            "md",
            "lg"
          ],
          "default": "md",
          "description": "Avatar size"
        }
      },
      "composition": {
        "allowedChildren": []
      },
      "a11yContract": {
        "handled": [
          "Image alt is forwarded from the alt attribute",
          "Falls back to initials derived from alt when the image fails to load"
        ],
        "required": [
          {
            "when": "src is set",
            "requirement": "Provide alt describing the person or entity (e.g. alt=\"Jane Doe\"). Empty alt is only acceptable for purely decorative avatars.",
            "severity": "error",
            "rule": "avatar-alt"
          },
          {
            "when": "src is not set and fallback is empty",
            "requirement": "Provide alt so initials can be derived, or set fallback explicitly.",
            "severity": "error",
            "rule": "avatar-fallback"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-avatar src=\"/u.jpg\"></aihio-avatar>",
          "reason": "Missing alt. Screen readers will announce only the filename.",
          "fix": "<aihio-avatar src=\"/u.jpg\" alt=\"Jane Doe\"></aihio-avatar>",
          "rule": "avatar-alt"
        },
        {
          "markup": "<aihio-avatar>JD</aihio-avatar>",
          "reason": "Avatar renders its own content; passed children are replaced. Use the fallback attribute instead.",
          "fix": "<aihio-avatar fallback=\"JD\" alt=\"Jane Doe\"></aihio-avatar>",
          "rule": "invalid-child"
        }
      ],
      "examples": [
        {
          "title": "With an image",
          "description": "alt names the person for screen readers.",
          "markup": "<aihio-avatar src=\"/avatar.jpg\" alt=\"Jane Doe\"></aihio-avatar>"
        },
        {
          "title": "Initials from the name",
          "description": "With no src, the initials are taken from alt.",
          "markup": "<aihio-avatar alt=\"Jane Doe\"></aihio-avatar>"
        },
        {
          "title": "Explicit initials, large",
          "description": "fallback sets the initials directly.",
          "markup": "<aihio-avatar fallback=\"JD\" size=\"lg\"></aihio-avatar>"
        }
      ]
    },
    {
      "$component": "aihio-badge",
      "version": "1.1.0",
      "description": "A small non-interactive label for a status, count, or category, such as Live, Beta, or 3 new.",
      "intents": [
        "status",
        "labeling",
        "metadata"
      ],
      "attributes": {
        "variant": {
          "type": "enum",
          "values": [
            "default",
            "secondary",
            "outline",
            "success",
            "warning",
            "destructive"
          ],
          "default": "default",
          "description": "Visual style variant"
        }
      },
      "slots": {
        "default": {
          "description": "Badge text content"
        }
      },
      "composition": {
        "allowedChildren": [
          "#text"
        ],
        "forbiddenChildren": [
          "aihio-button",
          "aihio-input",
          "aihio-dialog"
        ]
      },
      "a11yContract": {
        "handled": [
          "Badge stays semantically neutral: it does not add button, alert, status, or live-region roles automatically."
        ],
        "required": [
          {
            "when": "badge conveys information not present elsewhere (e.g. unread count)",
            "requirement": "Include the meaning in visible text or an aria-label on the surrounding context. Badges have no inherent role.",
            "severity": "warn"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-badge><aihio-button>Delete</aihio-button></aihio-badge>",
          "reason": "Badges are non-interactive labels. Put interactive elements outside the badge.",
          "fix": "<aihio-cluster gap=\"tight\">\n  <aihio-badge>Draft</aihio-badge>\n  <aihio-button variant=\"ghost\" size=\"sm\">Delete</aihio-button>\n</aihio-cluster>",
          "rule": "forbidden-descendant"
        },
        {
          "markup": "<aihio-badge variant=\"primary\">New</aihio-badge>",
          "reason": "variant=\"primary\" is not valid. Use variant=\"default\" for the primary style.",
          "fix": "<aihio-badge variant=\"default\">New</aihio-badge>",
          "rule": "invalid-enum-attribute"
        }
      ],
      "examples": [
        {
          "title": "Default",
          "markup": "<aihio-badge>New</aihio-badge>"
        },
        {
          "title": "Secondary",
          "markup": "<aihio-badge variant=\"secondary\">Draft</aihio-badge>"
        },
        {
          "title": "Outline",
          "markup": "<aihio-badge variant=\"outline\">v2.0</aihio-badge>"
        },
        {
          "title": "Destructive",
          "markup": "<aihio-badge variant=\"destructive\">Error</aihio-badge>"
        }
      ]
    },
    {
      "$component": "aihio-button",
      "version": "1.5.0",
      "description": "An action the user takes: submit a form, open a dialog, run a command. Renders a real <button>; variant=\"default\" is the primary style and variant=\"destructive\" marks an irreversible action. To go to another page, put an <a href> inside it instead: the link becomes the control, styled as the button.",
      "intents": [
        "action",
        "primary-action",
        "secondary-action",
        "destructive-action",
        "navigation"
      ],
      "attributes": {
        "variant": {
          "type": "enum",
          "values": [
            "default",
            "secondary",
            "outline",
            "ghost",
            "link",
            "destructive"
          ],
          "default": "default",
          "description": "Visual style variant"
        },
        "size": {
          "type": "enum",
          "values": [
            "sm",
            "md",
            "lg",
            "icon"
          ],
          "default": "md",
          "description": "Button size"
        },
        "disabled": {
          "type": "boolean",
          "default": false,
          "description": "Disables the button"
        },
        "loading": {
          "type": "boolean",
          "default": false,
          "description": "Shows loading state and disables interaction"
        },
        "type": {
          "type": "enum",
          "values": [
            "button",
            "submit",
            "reset"
          ],
          "default": "button",
          "description": "Form behaviour, forwarded to the native <button> this component renders. submit submits the owning form (running native constraint validation first); reset resets it. Defaults to button, which does neither."
        },
        "name": {
          "type": "string",
          "description": "Submitter name included with form data."
        },
        "value": {
          "type": "string",
          "description": "Submitter value included with form data."
        },
        "form": {
          "type": "string",
          "description": "Id of an external form that owns the button."
        },
        "formaction": {
          "type": "string",
          "description": "Submit URL override for submit buttons."
        },
        "formmethod": {
          "type": "enum",
          "values": [
            "get",
            "post",
            "dialog"
          ],
          "description": "HTTP method override for submit buttons."
        },
        "formenctype": {
          "type": "enum",
          "values": [
            "application/x-www-form-urlencoded",
            "multipart/form-data",
            "text/plain"
          ],
          "description": "Encoding override for submit buttons."
        },
        "formnovalidate": {
          "type": "boolean",
          "description": "Skips form validation when this submit button is used."
        },
        "formtarget": {
          "type": "string",
          "description": "Browsing context target for the form response."
        },
        "command": {
          "type": "string",
          "description": "Invoker command sent to the element named by commandfor when the button is activated, e.g. command=\"--open\" to open an aihio-dialog. Aihio components take --prefixed custom commands; see each component's commands."
        },
        "commandfor": {
          "type": "string",
          "description": "Id of the element that receives command. Use with command to open or close an overlay without script."
        }
      },
      "slots": {
        "default": {
          "description": "Button label content"
        }
      },
      "events": {
        "click": {
          "description": "Fired when the button is clicked"
        }
      },
      "composition": {
        "forbiddenChildren": [
          "aihio-button",
          "aihio-input",
          "aihio-dialog",
          "aihio-dropdown"
        ]
      },
      "a11yContract": {
        "handled": [
          "The component renders a real <button> and forwards its attributes to it, so the role, Enter and Space activation, focus behaviour, disabled semantics, and the forced-colours mapping are the platform's rather than an emulation of it",
          "An authored <a> child becomes the control instead of a <button>, so a call to action that goes to another page is a real link: it is announced as one, shows its URL, opens in a new tab, and works before the module loads",
          "aria-* set on the host is mirrored onto that control",
          "A disabled or loading link gets aria-disabled=\"true\", leaves the tab order, and its clicks are cancelled, since a link has no native disabled state",
          "aria-busy reflects loading state, which also disables the control",
          "Form participation is native: the button is a submitter of its owning form, so Enter in a field submits, SubmitEvent.submitter identifies it, and <fieldset disabled> disables it in both directions",
          "type=\"submit\" runs native constraint validation before the form submits"
        ],
        "required": [
          {
            "when": "size=\"icon\" or the button has no visible text",
            "requirement": "Provide aria-label describing the action (e.g. aria-label=\"Close\").",
            "severity": "error",
            "rule": "button-accessible-name"
          },
          {
            "when": "the button submits a form",
            "requirement": "Set type=\"submit\" and either place the button inside its <form> or reference that form with the form attribute.",
            "severity": "warn",
            "rule": "button-form-owner"
          },
          {
            "when": "the button goes to another page from a click handler (onclick sets location, or calls window.open or router.push)",
            "requirement": "Put an <a href> inside aihio-button instead: <aihio-button><a href=\"/pricing\">See pricing</a></aihio-button>. A button that navigates is announced as a button, shows no URL, cannot be opened in a new tab, and does nothing without script.",
            "severity": "error",
            "rule": "button-link-navigation"
          },
          {
            "when": "the button wraps an <a>",
            "requirement": "Give the <a> an href. Without one it is not a link: it has no role, takes no focus, and goes nowhere.",
            "severity": "error",
            "rule": "button-link-href"
          },
          {
            "when": "the button wraps an <a>",
            "requirement": "Leave type, name, value, form*, command, and commandfor off the host. They configure a <button>, so on a link they do nothing.",
            "severity": "warn",
            "rule": "button-link-attributes"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-button variant=\"primary\">Save</aihio-button>",
          "reason": "variant=\"primary\" is not valid. The primary style is variant=\"default\".",
          "fix": "<aihio-button variant=\"default\">Save</aihio-button>",
          "rule": "invalid-enum-attribute"
        },
        {
          "markup": "<aihio-button><aihio-button>Save</aihio-button></aihio-button>",
          "reason": "Buttons must not be nested. Use sibling buttons or aihio-dropdown for grouped actions.",
          "fix": "<aihio-cluster>\n  <aihio-button variant=\"outline\">Cancel</aihio-button>\n  <aihio-button>Save</aihio-button>\n</aihio-cluster>",
          "rule": "forbidden-descendant"
        },
        {
          "markup": "<aihio-button size=\"icon\">&#x2715;</aihio-button>",
          "reason": "Icon-only button is missing an accessible name. Add aria-label=\"Close\" (or similar).",
          "fix": "<aihio-button size=\"icon\" aria-label=\"Close\">&#x2715;</aihio-button>",
          "rule": "button-accessible-name"
        },
        {
          "markup": "<form>\n  <aihio-input name=\"email\" aria-label=\"Email\"></aihio-input>\n</form>\n<aihio-button type=\"submit\">Sign in</aihio-button>",
          "reason": "A submit button outside the <form> never submits it. Move the button inside the form element.",
          "fix": "<form>\n  <aihio-input name=\"email\" aria-label=\"Email\"></aihio-input>\n  <aihio-button type=\"submit\">Sign in</aihio-button>\n</form>",
          "rule": "button-form-owner"
        },
        {
          "markup": "<aihio-button onclick=\"location.href='/pricing'\">See pricing</aihio-button>",
          "reason": "A button that navigates is announced as a button, shows no URL, cannot be opened in a new tab, and does nothing without script. Put a link inside it: <aihio-button><a href=\"/pricing\">See pricing</a></aihio-button>.",
          "fix": "<aihio-button><a href=\"/pricing\">See pricing</a></aihio-button>",
          "rule": "button-link-navigation"
        },
        {
          "markup": "<aihio-button href=\"/pricing\">See pricing</aihio-button>",
          "reason": "aihio-button has no href; nothing reads it. Put the link inside: <aihio-button><a href=\"/pricing\">See pricing</a></aihio-button>.",
          "fix": "<aihio-button><a href=\"/pricing\">See pricing</a></aihio-button>",
          "rule": "unknown-attribute"
        }
      ],
      "examples": [
        {
          "title": "Primary action",
          "description": "The default variant, for the main thing to do in a view.",
          "markup": "<aihio-button>Click me</aihio-button>"
        },
        {
          "title": "Small and outlined",
          "description": "A lower-emphasis action beside a primary one.",
          "markup": "<aihio-button variant=\"outline\" size=\"sm\">Cancel</aihio-button>"
        },
        {
          "title": "Destructive",
          "description": "For an action that deletes something or cannot be undone.",
          "markup": "<aihio-button variant=\"destructive\">Delete</aihio-button>"
        },
        {
          "title": "Icon only",
          "description": "With no visible text, aria-label is its name.",
          "markup": "<aihio-button size=\"icon\" aria-label=\"Close\">&#x2715;</aihio-button>"
        },
        {
          "title": "A link drawn as a button",
          "description": "A call to action that goes to another page wraps an <a href>.",
          "markup": "<aihio-button variant=\"outline\"><a href=\"/pricing\">See pricing</a></aihio-button>"
        },
        {
          "title": "Submits its form",
          "description": "type=\"submit\" makes it the form's submitter, so Enter in a field submits too.",
          "markup": "<form>\n  <label>\n    Email\n    <aihio-input type=\"email\" name=\"email\" required></aihio-input>\n  </label>\n  <aihio-button type=\"submit\">Sign in</aihio-button>\n</form>"
        },
        {
          "title": "Opens a dialog from markup",
          "description": "commandfor names the dialog and command=\"--open\" opens it, with no script.",
          "markup": "<aihio-button commandfor=\"confirm-delete\" command=\"--open\" variant=\"destructive\">Delete project</aihio-button>\n\n<aihio-dialog id=\"confirm-delete\">\n  <aihio-dialog-header>\n    <aihio-dialog-title>Delete project?</aihio-dialog-title>\n  </aihio-dialog-header>\n  <form method=\"post\">\n    <aihio-dialog-footer>\n      <aihio-button commandfor=\"confirm-delete\" command=\"--close\" variant=\"outline\">Cancel</aihio-button>\n      <aihio-button type=\"submit\" variant=\"destructive\">Delete project</aihio-button>\n    </aihio-dialog-footer>\n  </form>\n</aihio-dialog>"
        }
      ],
      "properties": {
        "control": {
          "type": "HTMLButtonElement | HTMLAnchorElement | null",
          "readonly": true,
          "description": "The native button the component delegates to, or the authored <a> when it wraps a link"
        },
        "form": {
          "type": "HTMLFormElement | null",
          "readonly": true,
          "description": "The owning form, or null outside one or when the control is a link"
        },
        "type": {
          "type": "\"button\" | \"submit\" | \"reset\"",
          "description": "Current native button type"
        }
      },
      "methods": {
        "click()": {
          "description": "Activates the native button.",
          "returns": "void"
        },
        "focus()": {
          "description": "Moves focus to the native button.",
          "parameters": [
            {
              "name": "options",
              "type": "FocusOptions",
              "optional": true
            }
          ],
          "returns": "void"
        },
        "blur()": {
          "description": "Removes focus from the native button.",
          "returns": "void"
        }
      }
    },
    {
      "$component": "aihio-card",
      "version": "1.0.0",
      "description": "A bordered surface that groups related content, with an optional header (title and description), content area, and footer for actions, such as a settings section or an item in a grid.",
      "intents": [
        "container",
        "surface"
      ],
      "attributes": {
        "variant": {
          "type": "enum",
          "values": [
            "default",
            "outline"
          ],
          "default": "default",
          "description": "Visual style variant. Outline removes the shadow."
        }
      },
      "slots": {
        "default": {
          "description": "Card content. Use aihio-card-header, aihio-card-content, aihio-card-footer for structure."
        }
      },
      "composition": {
        "allowedChildren": [
          "aihio-card-header",
          "aihio-card-content",
          "aihio-card-footer",
          "#flow"
        ]
      },
      "a11yContract": {
        "handled": [
          "Card only adds visual grouping; headings, paragraphs, links, and buttons inside keep their native semantics."
        ],
        "required": [
          {
            "when": "card acts as a link or button (entire surface is clickable, or it has a click handler)",
            "requirement": "Wrap the card in an <a> or attach role=\"button\" + keyboard handlers on the outer element. aihio-card itself has no interactive role.",
            "severity": "warn",
            "rule": "card-click-handler"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-card><aihio-card-title>Hi</aihio-card-title></aihio-card>",
          "reason": "Title must live inside aihio-card-header, not directly inside aihio-card.",
          "fix": "<aihio-card>\n  <aihio-card-header>\n    <aihio-card-title>Hi</aihio-card-title>\n  </aihio-card-header>\n</aihio-card>",
          "rule": "invalid-child"
        },
        {
          "markup": "<aihio-card onclick=\"openDetails()\"><p>Open details</p></aihio-card>",
          "reason": "Clickable cards need keyboard-accessible semantics. Wrap the card in a link, or add role=\"button\" plus keyboard handlers to the interactive surface.",
          "fix": "<a href=\"/details\">\n  <aihio-card><p>Open details</p></aihio-card>\n</a>",
          "rule": "card-click-handler"
        }
      ],
      "related": [
        {
          "$component": "aihio-card-header",
          "description": "Card header container"
        },
        {
          "$component": "aihio-card-title",
          "description": "Card title text"
        },
        {
          "$component": "aihio-card-description",
          "description": "Card description text"
        },
        {
          "$component": "aihio-card-content",
          "description": "Card main content area"
        },
        {
          "$component": "aihio-card-footer",
          "description": "Card footer with actions"
        }
      ],
      "examples": [
        {
          "title": "Header, content, and footer",
          "markup": "<aihio-card>\n  <aihio-card-header>\n    <aihio-card-title>Card Title</aihio-card-title>\n    <aihio-card-description>Card description.</aihio-card-description>\n  </aihio-card-header>\n  <aihio-card-content>\n    <p>Card content here.</p>\n  </aihio-card-content>\n  <aihio-card-footer>\n    <aihio-button>Action</aihio-button>\n  </aihio-card-footer>\n</aihio-card>"
        }
      ]
    },
    {
      "$component": "aihio-cluster",
      "version": "1.0.0",
      "description": "Horizontal layout primitive for grouped inline items such as button rows, badge lists, and toolbars. Wraps when space runs out.",
      "intents": [
        "layout",
        "container"
      ],
      "attributes": {
        "gap": {
          "type": "enum",
          "values": [
            "tight",
            "sm",
            "md",
            "lg"
          ],
          "default": "sm",
          "description": "Spacing between items, from the spacing tokens."
        },
        "align": {
          "type": "enum",
          "values": [
            "start",
            "center",
            "end",
            "baseline"
          ],
          "default": "center",
          "description": "Cross-axis alignment of items."
        },
        "justify": {
          "type": "enum",
          "values": [
            "start",
            "center",
            "end",
            "between"
          ],
          "default": "start",
          "description": "Main-axis distribution of items."
        },
        "grow": {
          "type": "boolean",
          "default": false,
          "description": "Makes the cluster fill its row. Needed for justify to have any effect inside a flex parent such as a card or dialog footer, where the cluster is otherwise sized to its content."
        }
      },
      "slots": {
        "default": {
          "description": "Inline items to group, typically buttons, badges, or avatars."
        }
      },
      "composition": {
        "allowedChildren": [
          "*"
        ]
      },
      "a11yContract": {
        "handled": [
          "Purely presentational: adds no role and no tab stop, so grouped controls keep their own semantics and focus order.",
          "Items wrap rather than overflow, so grouped actions stay reachable at narrow widths."
        ],
        "required": [
          {
            "when": "the cluster is a toolbar of related controls",
            "requirement": "Add role=\"toolbar\" and an aria-label on the cluster; it is presentational by default.",
            "severity": "warn"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-cluster justify=\"space-between\"><aihio-button>Save</aihio-button></aihio-cluster>",
          "reason": "justify=\"space-between\" is not valid. Use justify=\"between\".",
          "fix": "<aihio-cluster justify=\"between\"><aihio-button>Save</aihio-button></aihio-cluster>",
          "rule": "invalid-enum-attribute"
        },
        {
          "markup": "<aihio-stack><aihio-button>Cancel</aihio-button><aihio-button>Save</aihio-button></aihio-stack>",
          "reason": "A row of actions belongs in aihio-cluster. aihio-stack stacks them vertically and stretches them to full width.",
          "fix": "<aihio-cluster><aihio-button>Cancel</aihio-button><aihio-button>Save</aihio-button></aihio-cluster>"
        },
        {
          "markup": "<aihio-card-footer>\n  <aihio-cluster justify=\"end\">\n    <aihio-button>Save</aihio-button>\n  </aihio-cluster>\n</aihio-card-footer>",
          "reason": "justify=\"end\" does nothing here: the cluster is a content-sized flex item inside the footer. Add grow so it fills the row.",
          "fix": "<aihio-card-footer>\n  <aihio-cluster grow justify=\"end\">\n    <aihio-button>Save</aihio-button>\n  </aihio-cluster>\n</aihio-card-footer>",
          "rule": "cluster-needs-grow"
        }
      ],
      "examples": [
        {
          "title": "Form actions at the end of a row",
          "markup": "<form>\n  <aihio-cluster justify=\"end\">\n    <aihio-button variant=\"outline\" type=\"reset\">Reset</aihio-button>\n    <aihio-button type=\"submit\">Save</aihio-button>\n  </aihio-cluster>\n</form>"
        },
        {
          "title": "Badges, tightly spaced",
          "markup": "<aihio-cluster gap=\"tight\">\n  <aihio-badge>Draft</aihio-badge>\n  <aihio-badge variant=\"secondary\">Internal</aihio-badge>\n</aihio-cluster>"
        },
        {
          "title": "In a card footer",
          "description": "A cluster in a footer has to grow to fill the row before justify has room to act.",
          "markup": "<aihio-card>\n  <form>\n    <aihio-card-content>\n      <aihio-field>\n        <label slot=\"label\">Project name</label>\n        <aihio-input name=\"project\" value=\"Aihio\"></aihio-input>\n      </aihio-field>\n    </aihio-card-content>\n    <aihio-card-footer>\n      <aihio-cluster grow justify=\"end\">\n        <aihio-button variant=\"outline\" type=\"reset\">Reset</aihio-button>\n        <aihio-button type=\"submit\">Save</aihio-button>\n      </aihio-cluster>\n    </aihio-card-footer>\n  </form>\n</aihio-card>"
        }
      ]
    },
    {
      "$component": "aihio-combobox",
      "version": "1.0.0",
      "description": "A text field for picking one option from a long list: typing filters the options, and the chosen option's value submits with the form. Use it when there are too many options to scan comfortably (roughly more than ten), or when the options come from a search.",
      "intents": [
        "form-field",
        "selection",
        "text-entry"
      ],
      "attributes": {
        "value": {
          "type": "string",
          "description": "Value of the initially chosen option, and the value a form reset returns to. The value property carries the live choice."
        },
        "name": {
          "type": "string",
          "description": "Form field name. The chosen option's value (not its label) is submitted under it. Without it the field submits nothing."
        },
        "form": {
          "type": "string",
          "description": "Id of an external form that owns the field."
        },
        "placeholder": {
          "type": "string",
          "description": "Placeholder text. It is not a label."
        },
        "disabled": {
          "type": "boolean",
          "default": false,
          "description": "Disables the field. A disabled field submits nothing."
        },
        "readonly": {
          "type": "boolean",
          "default": false,
          "description": "Shows the chosen option without letting it change. The value still submits."
        },
        "required": {
          "type": "boolean",
          "default": false,
          "description": "Requires a choice for native constraint validation."
        },
        "error": {
          "type": "boolean",
          "default": false,
          "description": "Shows error styling and sets aria-invalid. aihio-field sets this from its error slot."
        },
        "size": {
          "type": "enum",
          "values": [
            "sm",
            "md",
            "lg"
          ],
          "default": "md",
          "description": "Field size, matching aihio-input."
        },
        "filter": {
          "type": "enum",
          "values": [
            "contains",
            "starts-with",
            "none"
          ],
          "default": "contains",
          "description": "How typing filters the options. contains ranks prefix matches first, then word-start matches, then any match. none shows every option as given, for lists filtered by a server."
        },
        "allow-custom": {
          "type": "boolean",
          "default": false,
          "description": "Accepts typed text that matches no option; the text becomes the value. Without it, leaving the field reverts unmatched text to the chosen option."
        },
        "loading": {
          "type": "boolean",
          "default": false,
          "description": "Shows a loading row and marks the list busy while options are being fetched."
        },
        "empty-text": {
          "type": "string",
          "default": "No results",
          "description": "Shown and announced when no option matches."
        },
        "loading-text": {
          "type": "string",
          "default": "Loading…",
          "description": "Shown and announced while loading is set."
        },
        "results-text": {
          "type": "string",
          "description": "Result-count announcement with a {count} placeholder, for localisation (e.g. \"{count} tulosta\"). Defaults to \"1 result\" / \"N results\"."
        },
        "open": {
          "type": "boolean",
          "default": false,
          "description": "Reflects whether the list is showing. Set or remove it to open or close the list."
        }
      },
      "properties": {
        "value": {
          "type": "string",
          "description": "Get or set the chosen option's value (or the free text, with allow-custom). Setting it does not fire aihio-change."
        },
        "defaultValue": {
          "type": "string",
          "description": "Get or set the reset value reflected by the value attribute"
        },
        "selectedOption": {
          "type": "HTMLElement | null",
          "readonly": true,
          "description": "The chosen aihio-option, or null"
        },
        "control": {
          "type": "HTMLInputElement | null",
          "readonly": true,
          "description": "The native text input carrying role=\"combobox\""
        },
        "form": {
          "type": "HTMLFormElement | null",
          "readonly": true,
          "description": "The owning form, or null outside one"
        },
        "validity": {
          "type": "ValidityState | null",
          "readonly": true,
          "description": "Native constraint validation state"
        },
        "validationMessage": {
          "type": "string",
          "readonly": true,
          "description": "Native validation message"
        },
        "willValidate": {
          "type": "boolean",
          "readonly": true,
          "description": "Whether the field participates in constraint validation"
        }
      },
      "methods": {
        "open()": {
          "description": "Opens the list showing every option, with the chosen one highlighted.",
          "returns": "void"
        },
        "close()": {
          "description": "Closes the list, reverting unconfirmed text to the chosen option.",
          "returns": "void"
        },
        "toggle()": {
          "description": "Opens or closes the list.",
          "returns": "void"
        },
        "focus()": {
          "description": "Moves focus to the text input.",
          "parameters": [
            {
              "name": "options",
              "type": "FocusOptions",
              "optional": true
            }
          ],
          "returns": "void"
        },
        "checkValidity()": {
          "description": "Runs native constraint validation and returns whether the field is valid.",
          "returns": "boolean"
        },
        "reportValidity()": {
          "description": "Runs native constraint validation and shows the browser message if invalid.",
          "returns": "boolean"
        },
        "setCustomValidity()": {
          "description": "Sets a custom validation message.",
          "parameters": [
            {
              "name": "message",
              "type": "string"
            }
          ],
          "returns": "void"
        }
      },
      "events": {
        "aihio-change": {
          "description": "Fired when the person commits a different choice, or when a form reset changes it. Not fired for programmatic value changes.",
          "detail": {
            "value": "string",
            "label": "string"
          },
          "bubbles": true,
          "composed": true
        },
        "aihio-search": {
          "description": "Fired on every keystroke with the typed text. Use it to fetch options for filter=\"none\".",
          "detail": {
            "query": "string"
          },
          "bubbles": true,
          "composed": true
        },
        "aihio-open": {
          "description": "Fired after the list opens.",
          "bubbles": true,
          "composed": true
        },
        "aihio-close": {
          "description": "Fired after the list closes.",
          "detail": {
            "reason": "string"
          },
          "bubbles": true,
          "composed": true
        }
      },
      "composition": {
        "allowedChildren": [
          "aihio-option"
        ]
      },
      "a11yContract": {
        "handled": [
          "A real <input role=\"combobox\"> with aria-autocomplete=\"list\", aria-expanded, and aria-controls pointing at the listbox",
          "DOM focus stays in the input; aria-activedescendant follows the highlighted option, in the same tree as the options it names",
          "Options render as role=\"option\" inside role=\"listbox\"; the chosen option has aria-selected=\"true\" and a check mark, disabled options have aria-disabled",
          "ArrowDown/ArrowUp open the list and move through enabled options (wrapping); Alt+ArrowDown opens without moving; Alt+ArrowUp accepts and closes; PageDown/PageUp jump ten",
          "Enter chooses the highlighted option; Escape closes and reverts unconfirmed text, and is left alone when the list is closed so a surrounding dialog still closes",
          "Tab accepts the highlighted option and moves on; leaving the field any other way keeps the previous choice unless the text matches an option exactly",
          "Typing filters case- and accent-insensitively (\"aland\" finds \"Åland\"), ranks prefix matches first, highlights the first match, and sets the matched text in bold",
          "A polite status region announces the result count, the empty message, or the loading message once typing pauses",
          "The toggle button is out of the tab order, carries the field's label, and exposes aria-expanded",
          "Keys pressed while an IME is composing are left to the IME",
          "The chosen value submits through a hidden input under name; required, form reset, <fieldset disabled>, and the form attribute behave natively",
          "The list renders in the top layer, flips above the field when there is more room there, and fits the visual viewport above an on-screen keyboard",
          "Forced colours restate the highlighted and disabled states with system colours; reduced motion removes the open animation and spinner"
        ],
        "required": [
          {
            "when": "the combobox is not inside an aihio-field",
            "requirement": "Label it with aria-labelledby pointing at visible text, or aria-label. Placeholder is not a label, and a wrapping <label> would fold the option text into the field's name.",
            "severity": "error",
            "rule": "combobox-label"
          },
          {
            "when": "two aihio-option children share a value",
            "requirement": "Give every option a distinct value; the value is how the chosen option is found again.",
            "severity": "error",
            "rule": "combobox-option-values"
          },
          {
            "when": "the combobox is inside a <form> and its value should be submitted",
            "requirement": "Set name. A combobox without a name is omitted from FormData.",
            "severity": "warn",
            "rule": "combobox-form-name"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-combobox placeholder=\"Country\">\n  <aihio-option value=\"fi\">Finland</aihio-option>\n</aihio-combobox>",
          "reason": "Placeholder is not a label. Wrap it in aihio-field with a label slot, or add aria-labelledby or aria-label.",
          "fix": "<aihio-field>\n  <label slot=\"label\">Country</label>\n  <aihio-combobox name=\"country\">\n    <aihio-option value=\"fi\">Finland</aihio-option>\n  </aihio-combobox>\n</aihio-field>",
          "rule": "combobox-label"
        },
        {
          "markup": "<label>\n  Country\n  <aihio-combobox>\n    <aihio-option value=\"fi\">Finland</aihio-option>\n  </aihio-combobox>\n</label>",
          "reason": "A wrapping <label> takes its name from all of its text, so an open list's options become part of the field's name. Use aihio-field instead.",
          "fix": "<aihio-field>\n  <label slot=\"label\">Country</label>\n  <aihio-combobox name=\"country\">\n    <aihio-option value=\"fi\">Finland</aihio-option>\n  </aihio-combobox>\n</aihio-field>",
          "rule": "combobox-label"
        },
        {
          "markup": "<aihio-combobox aria-label=\"Size\">\n  <option value=\"s\">Small</option>\n  <option value=\"m\">Medium</option>\n</aihio-combobox>",
          "reason": "Native <option> elements are not read. Use aihio-option.",
          "fix": "<aihio-combobox aria-label=\"Size\">\n  <aihio-option value=\"s\">Small</aihio-option>\n  <aihio-option value=\"m\">Medium</aihio-option>\n</aihio-combobox>",
          "rule": "invalid-child"
        },
        {
          "markup": "<aihio-combobox aria-label=\"Plan\">\n  <aihio-option>Pro</aihio-option>\n  <aihio-option value=\"Pro\">Pro (annual)</aihio-option>\n</aihio-combobox>",
          "reason": "An option without a value uses its label as its value, so both options have the value \"Pro\" and the second can never be told apart from the first.",
          "fix": "<aihio-combobox aria-label=\"Plan\">\n  <aihio-option value=\"pro\">Pro</aihio-option>\n  <aihio-option value=\"pro-annual\">Pro (annual)</aihio-option>\n</aihio-combobox>",
          "rule": "combobox-option-values"
        }
      ],
      "related": [
        {
          "$component": "aihio-option",
          "description": "One choice in an aihio-combobox. Its text (or label attribute) is what typing matches and what the field shows once chosen.",
          "attributes": {
            "value": {
              "type": "string",
              "description": "Submitted value. Defaults to the label."
            },
            "label": {
              "type": "string",
              "description": "Text matched against and shown in the field when the content is richer than a plain label (an icon, a secondary line)."
            },
            "disabled": {
              "type": "boolean",
              "default": false,
              "description": "Shown but cannot be chosen, and skipped by the arrow keys."
            }
          },
          "properties": {
            "value": {
              "type": "string",
              "description": "The value attribute, or the label when it is absent"
            },
            "label": {
              "type": "string",
              "description": "The label attribute, or the normalised text content"
            },
            "disabled": {
              "type": "boolean",
              "description": "Reflects the disabled attribute"
            },
            "selected": {
              "type": "boolean",
              "readonly": true,
              "description": "Whether the owning combobox has this option chosen"
            }
          }
        }
      ],
      "examples": [
        {
          "title": "A searchable list",
          "description": "Typing filters without regard to case or accents.",
          "markup": "<aihio-field>\n  <label slot=\"label\">Country</label>\n  <aihio-combobox name=\"country\" placeholder=\"Search countries\">\n    <aihio-option value=\"fi\">Finland</aihio-option>\n    <aihio-option value=\"fr\">France</aihio-option>\n    <aihio-option value=\"de\">Germany</aihio-option>\n    <aihio-option value=\"is\">Iceland</aihio-option>\n    <aihio-option value=\"no\">Norway</aihio-option>\n    <aihio-option value=\"se\">Sweden</aihio-option>\n    <aihio-option value=\"ax\">Åland Islands</aihio-option>\n  </aihio-combobox>\n  <span slot=\"description\">Type to filter. Accents are optional.</span>\n</aihio-field>"
        },
        {
          "title": "Rich options and a preset value",
          "description": "label sets the text shown once an option is chosen; a disabled option cannot be.",
          "markup": "<aihio-field>\n  <label slot=\"label\">Assignee</label>\n  <aihio-combobox name=\"assignee\" value=\"ada\">\n    <aihio-option value=\"ada\" label=\"Ada Lovelace\">Ada Lovelace <small>Engineering</small></aihio-option>\n    <aihio-option value=\"grace\" label=\"Grace Hopper\">Grace Hopper <small>Compilers</small></aihio-option>\n    <aihio-option value=\"alan\" label=\"Alan Turing\" disabled>Alan Turing <small>On leave</small></aihio-option>\n  </aihio-combobox>\n</aihio-field>"
        },
        {
          "title": "Free text allowed",
          "description": "allow-custom keeps text that matches no option.",
          "markup": "<aihio-field>\n  <label slot=\"label\">Tag</label>\n  <aihio-combobox name=\"tag\" allow-custom placeholder=\"Pick or type a tag\">\n    <aihio-option>bug</aihio-option>\n    <aihio-option>design</aihio-option>\n    <aihio-option>documentation</aihio-option>\n  </aihio-combobox>\n</aihio-field>"
        },
        {
          "title": "Options from a server",
          "description": "filter=\"none\" leaves filtering to you: listen for aihio-search and replace the options.",
          "markup": "<aihio-combobox aria-label=\"Search users\" name=\"user\" filter=\"none\" empty-text=\"Type to search\"></aihio-combobox>"
        }
      ]
    },
    {
      "$component": "aihio-data-grid",
      "version": "1.0.0",
      "description": "A virtualized data table for large datasets, more rows than a page can hold (thousands to hundreds of thousands): one scrolling box in which only the rows in view are rendered, by your code, when the grid asks for them. Arrow keys move between cells as in a spreadsheet, and the grid tells assistive technology how many rows there are and which ones it shows. Find in page, printing, and reading the rows in a screen reader's browse mode only reach the rendered rows; when people need those, use aihio-table with aihio-pagination instead.",
      "intents": [
        "tabular-data"
      ],
      "attributes": {
        "row-count": {
          "type": "number",
          "description": "How many rows there are in all. Setting it, as when a filter changes the rows, asks for the rows in view again."
        },
        "density": {
          "type": "enum",
          "values": [
            "default",
            "compact"
          ],
          "default": "default",
          "description": "Row height. compact fits more rows in the box."
        },
        "loading": {
          "type": "boolean",
          "default": false,
          "description": "Marks the rows as stale while new ones load: they are dimmed, and the table has aria-busy."
        },
        "sort-ascending-text": {
          "type": "string",
          "default": "Sorted by {column}, ascending",
          "description": "Announced after sorting a column ascending, with {column} for the header's text. For localisation."
        },
        "sort-descending-text": {
          "type": "string",
          "default": "Sorted by {column}, descending",
          "description": "Announced after sorting a column descending, with {column} for the header's text."
        }
      },
      "nativeElements": {
        "th": {
          "description": "A header cell. The grid uses its one header row for column headers, and keeps it in view.",
          "attributes": {
            "data-sortable": {
              "type": "string",
              "description": "Makes the column sortable: the header's content becomes a button, and a click fires aihio-sort for your code to sort the rows by this name."
            },
            "aria-sort": {
              "type": "enum",
              "values": [
                "ascending",
                "descending",
                "none",
                "other"
              ],
              "description": "The order your rows are in. The grid moves it from header to header as people sort."
            },
            "data-numeric": {
              "type": "boolean",
              "default": false,
              "description": "Lines a column of figures up by place value. Put it on the header and on each of the column's cells."
            }
          }
        },
        "td": {
          "description": "A data cell, one line high: text that does not fit ends in an ellipsis.",
          "attributes": {
            "data-numeric": {
              "type": "boolean",
              "default": false,
              "description": "Lines the figure up with the rest of its column."
            }
          }
        }
      },
      "properties": {
        "table": {
          "type": "HTMLTableElement | null",
          "readonly": true,
          "description": "The <table> the element enhances."
        },
        "body": {
          "type": "HTMLTableSectionElement | null",
          "readonly": true,
          "description": "The <tbody> your code renders the rows into. The grid keeps a section of its own above and below it, so table.tBodies[0] is not it."
        },
        "start": {
          "type": "number",
          "readonly": true,
          "description": "The first row asked for, counting from 0."
        },
        "end": {
          "type": "number",
          "readonly": true,
          "description": "The row after the last one asked for."
        },
        "rowCount": {
          "type": "number",
          "description": "Get or set row-count."
        }
      },
      "methods": {
        "scrollToRow()": {
          "description": "Scrolls so the row at index, counting from 0, is at the top of the view, and asks for the rows there.",
          "parameters": [
            {
              "name": "index",
              "type": "number"
            }
          ],
          "returns": "void"
        }
      },
      "events": {
        "aihio-range": {
          "description": "Asks for rows: render the rows from start up to (not including) end into body, in order, replacing those there. Fired when the grid connects, when its rows in view change, when row-count is set, and after a sort. Rows fetched for a range that is no longer start to end should be dropped.",
          "detail": {
            "start": "number",
            "end": "number"
          },
          "bubbles": true,
          "composed": true
        },
        "aihio-sort": {
          "description": "Fired when the person sorts by a column, once its header carries the new aria-sort. Sort your rows by column; the grid then scrolls to the top and asks for the rows again with aihio-range.",
          "detail": {
            "column": "string",
            "direction": "'ascending' | 'descending'"
          },
          "bubbles": true,
          "composed": true
        }
      },
      "composition": {
        "requiredChildren": [
          "table"
        ],
        "allowedChildren": [
          "table"
        ]
      },
      "a11yContract": {
        "handled": [
          "The native <table> becomes a grid with aria-rowcount for every row there is, and each rendered row carries aria-rowindex, so a screen reader says \"row 51,034 of 100,001\" while only the rows in view exist",
          "The grid is one tab stop. Arrow keys move between cells, Page Up and Page Down by the rows in view, Home and End to the ends of a row, and Control+Home and Control+End to the first and last cell",
          "Moving to a row that is not rendered scrolls it into view, waits for your code to render it, and then focuses it",
          "A cell holding one link or button is focused on that control; a cell holding several is entered with Enter or F2 and left with Escape. Every other control in the grid is out of the tab order",
          "Keys pressed in a text field or an open menu inside a cell are left to it",
          "If the row holding focus scrolls away and your code removes it, focus moves to the header of the same column rather than to the page",
          "The header stays in view, and sorts the way aihio-table's does: a button in the <th>, aria-sort on the <th>, and the new order announced in a polite status region",
          "Columns take their widths from <col> elements or the header cells, so they do not change as rows come and go, and each row is one line high",
          "Cells draw their focus ring inside their edge, where the scroll box cannot clip it",
          "loading sets aria-busy on the table while the rows are stale"
        ],
        "required": [
          {
            "when": "row-count is missing or not a whole number",
            "requirement": "Set row-count to how many rows there are in all. The grid asks for rows by number, and sizes its scroll box from the count.",
            "severity": "error",
            "rule": "data-grid-row-count"
          },
          {
            "when": "row-count is above 350,000",
            "requirement": "Narrow the rows with a filter, or show them a page at a time with aihio-table and aihio-pagination. Firefox stops a box's height at 17.9 million pixels, about 389,000 rows of the default height, and rows past it cannot be scrolled to.",
            "severity": "warn",
            "rule": "data-grid-row-limit"
          },
          {
            "when": "the table has no caption, aria-label, or aria-labelledby",
            "requirement": "Name the table: a <caption>, aria-labelledby pointing at the visible heading above it, or aria-label.",
            "severity": "error",
            "rule": "table-accessible-name"
          },
          {
            "when": "the table has no <th> cells",
            "requirement": "Name each column with a <th> in the <thead> row.",
            "severity": "error",
            "rule": "table-header-cells"
          },
          {
            "when": "a sortable header has no column name",
            "requirement": "Give each sortable header a name, data-sortable=\"duration\", so aihio-sort says which column to sort by without depending on the header's wording.",
            "severity": "warn",
            "rule": "table-sort-column-name"
          },
          {
            "when": "people need to find text in the page, print every row, or read the rows in a screen reader's browse mode",
            "requirement": "Use aihio-table with aihio-pagination instead. A grid has only the rows in view in the page, and those are all that find in page, printing, and browse mode reach.",
            "severity": "warn"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-data-grid>\n  <table aria-label=\"Requests\">\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable=\"id\">Request</th>\n        <th scope=\"col\" data-sortable=\"method\">Method</th>\n        <th scope=\"col\" data-sortable=\"path\">Path</th>\n        <th scope=\"col\" data-sortable=\"status\" data-numeric>Status</th>\n        <th scope=\"col\" data-sortable=\"duration\" data-numeric>Duration (ms)</th>\n      </tr>\n    </thead>\n    <tbody></tbody>\n  </table>\n</aihio-data-grid>",
          "reason": "Without row-count the grid does not know how many rows there are, so it asks for none and its scroll box has no height to scroll.",
          "fix": "<aihio-data-grid row-count=\"100000\">\n  <table aria-label=\"Requests\">\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable=\"id\">Request</th>\n        <th scope=\"col\" data-sortable=\"method\">Method</th>\n        <th scope=\"col\" data-sortable=\"path\">Path</th>\n        <th scope=\"col\" data-sortable=\"status\" data-numeric>Status</th>\n        <th scope=\"col\" data-sortable=\"duration\" data-numeric>Duration (ms)</th>\n      </tr>\n    </thead>\n    <tbody></tbody>\n  </table>\n</aihio-data-grid>",
          "rule": "data-grid-row-count"
        },
        {
          "markup": "<aihio-data-grid row-count=\"100000\">\n  <table>\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable=\"id\">Request</th>\n        <th scope=\"col\" data-sortable=\"method\">Method</th>\n        <th scope=\"col\" data-sortable=\"path\">Path</th>\n        <th scope=\"col\" data-sortable=\"status\" data-numeric>Status</th>\n        <th scope=\"col\" data-sortable=\"duration\" data-numeric>Duration (ms)</th>\n      </tr>\n    </thead>\n    <tbody></tbody>\n  </table>\n</aihio-data-grid>",
          "reason": "An unnamed grid is announced only as \"grid\", which says nothing about what its hundred thousand rows are.",
          "fix": "<aihio-data-grid row-count=\"100000\">\n  <table aria-label=\"Requests\">\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable=\"id\">Request</th>\n        <th scope=\"col\" data-sortable=\"method\">Method</th>\n        <th scope=\"col\" data-sortable=\"path\">Path</th>\n        <th scope=\"col\" data-sortable=\"status\" data-numeric>Status</th>\n        <th scope=\"col\" data-sortable=\"duration\" data-numeric>Duration (ms)</th>\n      </tr>\n    </thead>\n    <tbody></tbody>\n  </table>\n</aihio-data-grid>",
          "rule": "table-accessible-name"
        },
        {
          "markup": "<aihio-table row-count=\"100000\">\n  <table aria-label=\"Requests\">\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable=\"id\">Request</th>\n        <th scope=\"col\" data-sortable=\"method\">Method</th>\n        <th scope=\"col\" data-sortable=\"path\">Path</th>\n        <th scope=\"col\" data-sortable=\"status\" data-numeric>Status</th>\n        <th scope=\"col\" data-sortable=\"duration\" data-numeric>Duration (ms)</th>\n      </tr>\n    </thead>\n    <tbody></tbody>\n  </table>\n</aihio-table>",
          "reason": "aihio-table renders every row it is given, so it has no row count. A table of more rows than a page can hold is aihio-data-grid, or aihio-table with aihio-pagination.",
          "fix": "<aihio-data-grid row-count=\"100000\">\n  <table aria-label=\"Requests\">\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable=\"id\">Request</th>\n        <th scope=\"col\" data-sortable=\"method\">Method</th>\n        <th scope=\"col\" data-sortable=\"path\">Path</th>\n        <th scope=\"col\" data-sortable=\"status\" data-numeric>Status</th>\n        <th scope=\"col\" data-sortable=\"duration\" data-numeric>Duration (ms)</th>\n      </tr>\n    </thead>\n    <tbody></tbody>\n  </table>\n</aihio-data-grid>",
          "rule": "unknown-attribute"
        },
        {
          "markup": "<aihio-data-grid row-count=\"100000\">\n  <table aria-label=\"Requests\">\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable>Request</th>\n        <th scope=\"col\" data-sortable>Duration (ms)</th>\n      </tr>\n    </thead>\n    <tbody></tbody>\n  </table>\n</aihio-data-grid>",
          "reason": "Without a column name aihio-sort reports the header's text, which changes when the page is translated.",
          "fix": "<aihio-data-grid row-count=\"100000\">\n  <table aria-label=\"Requests\">\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable=\"id\">Request</th>\n        <th scope=\"col\" data-sortable=\"duration\">Duration (ms)</th>\n      </tr>\n    </thead>\n    <tbody></tbody>\n  </table>\n</aihio-data-grid>",
          "rule": "table-sort-column-name"
        },
        {
          "markup": "<aihio-data-grid row-count=\"2000000\">\n  <table aria-label=\"Requests\">\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable=\"id\">Request</th>\n        <th scope=\"col\" data-sortable=\"method\">Method</th>\n        <th scope=\"col\" data-sortable=\"path\">Path</th>\n        <th scope=\"col\" data-sortable=\"status\" data-numeric>Status</th>\n        <th scope=\"col\" data-sortable=\"duration\" data-numeric>Duration (ms)</th>\n      </tr>\n    </thead>\n    <tbody></tbody>\n  </table>\n</aihio-data-grid>",
          "reason": "Firefox stops a box's height at 17.9 million pixels, about 389,000 rows of the default height. The scroll ends there, before the rows do, and neither scrolling nor the keyboard reaches the rest.",
          "fix": "<aihio-table manual-sort>\n  <table aria-label=\"Requests\">\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable=\"id\" aria-sort=\"ascending\">Request</th>\n        <th scope=\"col\" data-sortable=\"method\">Method</th>\n        <th scope=\"col\" data-sortable=\"path\">Path</th>\n        <th scope=\"col\" data-sortable=\"status\" data-numeric>Status</th>\n        <th scope=\"col\" data-sortable=\"duration\" data-numeric>Duration (ms)</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr><th scope=\"row\">#1</th><td>GET</td><td>/api/items/0</td><td data-numeric>200</td><td data-numeric>12</td></tr>\n    </tbody>\n  </table>\n</aihio-table>\n<aihio-pagination page=\"1\" pages=\"80000\" href=\"/requests?page={page}\" aria-label=\"Request pages\"></aihio-pagination>",
          "rule": "data-grid-row-limit"
        }
      ],
      "examples": [
        {
          "title": "A hundred thousand rows",
          "description": "The rows live in script. aihio-range asks for the ones in view and the script renders them into body; aihio-sort asks for them in a new order. Try the arrow keys, Page Down, and Control+End.",
          "markup": "<aihio-data-grid id=\"requests-grid\" row-count=\"100000\" density=\"compact\">\n  <table aria-label=\"Requests\">\n    <colgroup>\n      <col style=\"width: 8rem\">\n      <col style=\"width: 6rem\">\n      <col>\n      <col style=\"width: 6rem\">\n      <col style=\"width: 9rem\">\n    </colgroup>\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable=\"id\" aria-sort=\"ascending\">Request</th>\n        <th scope=\"col\" data-sortable=\"method\">Method</th>\n        <th scope=\"col\" data-sortable=\"path\">Path</th>\n        <th scope=\"col\" data-sortable=\"status\" data-numeric>Status</th>\n        <th scope=\"col\" data-sortable=\"duration\" data-numeric>Duration (ms)</th>\n      </tr>\n    </thead>\n    <tbody></tbody>\n  </table>\n</aihio-data-grid>\n<script type=\"module\">\n  const grid = document.getElementById('requests-grid');\n  const methods = ['GET', 'POST', 'PATCH', 'DELETE'];\n  const rows = Array.from({ length: 100000 }, (_, index) => ({\n    id: index + 1,\n    method: methods[index % 4],\n    path: `/api/items/${(index * 7919) % 100000}`,\n    status: [200, 201, 204, 404, 500][index % 5],\n    duration: (index * 37) % 1500,\n  }));\n\n  function render(start, end) {\n    grid.body.replaceChildren(...rows.slice(start, end).map((row) => {\n      const tr = document.createElement('tr');\n      tr.innerHTML = `<th scope=\"row\">#${row.id}</th><td>${row.method}</td><td>${row.path}</td><td data-numeric>${row.status}</td><td data-numeric>${row.duration}</td>`;\n      return tr;\n    }));\n  }\n\n  grid.addEventListener('aihio-range', (event) => render(event.detail.start, event.detail.end));\n  grid.addEventListener('aihio-sort', (event) => {\n    const { column, direction } = event.detail;\n    const order = direction === 'ascending' ? 1 : -1;\n    rows.sort((a, b) => (a[column] > b[column] ? 1 : a[column] < b[column] ? -1 : 0) * order);\n  });\n  render(grid.start, grid.end);\n</script>"
        },
        {
          "title": "Rows from a server",
          "description": "Fetch the rows asked for, and mark the grid loading while they come. A response for rows no longer in view is dropped: the person has scrolled on.",
          "markup": "<aihio-data-grid id=\"orders-grid\" row-count=\"25000\">\n  <table aria-label=\"Orders\">\n    <colgroup>\n      <col style=\"width: 8rem\">\n      <col>\n      <col style=\"width: 9rem\">\n    </colgroup>\n    <thead>\n      <tr>\n        <th scope=\"col\">Order</th>\n        <th scope=\"col\">Customer</th>\n        <th scope=\"col\" data-numeric>Total</th>\n      </tr>\n    </thead>\n    <tbody></tbody>\n  </table>\n</aihio-data-grid>\n<script type=\"module\">\n  const grid = document.getElementById('orders-grid');\n\n  // Stands in for fetch('/api/orders?start=...&end=...').\n  const fetchOrders = (start, end) => new Promise((resolve) => setTimeout(() => resolve(\n    Array.from({ length: end - start }, (_, offset) => ({\n      id: 70000 + start + offset,\n      customer: `Customer ${((start + offset) * 31) % 997}`,\n      total: (((start + offset) * 7349) % 100000) / 100,\n    }))\n  ), 150));\n\n  async function load(start, end) {\n    grid.setAttribute('loading', '');\n    const orders = await fetchOrders(start, end);\n    if (start !== grid.start || end !== grid.end) return;\n    grid.body.replaceChildren(...orders.map((order) => {\n      const tr = document.createElement('tr');\n      tr.innerHTML = `<th scope=\"row\">#${order.id}</th><td>${order.customer}</td><td data-numeric>€${order.total.toFixed(2)}</td>`;\n      return tr;\n    }));\n    grid.removeAttribute('loading');\n  }\n\n  grid.addEventListener('aihio-range', (event) => load(event.detail.start, event.detail.end));\n  load(grid.start, grid.end);\n</script>"
        }
      ]
    },
    {
      "$component": "aihio-dialog",
      "version": "1.2.0",
      "description": "A modal dialog for a focused task or a confirmation before an irreversible action, such as deleting a project. Opens and closes from markup: commandfor plus command=\"--open\" or command=\"--close\".",
      "intents": [
        "overlay",
        "modal",
        "dismissible"
      ],
      "attributes": {
        "open": {
          "type": "boolean",
          "default": false,
          "description": "Whether the dialog is open"
        }
      },
      "methods": {
        "open()": {
          "description": "Opens the dialog modally in the browser top layer.",
          "returns": "void"
        },
        "close()": {
          "description": "Requests that the dialog close.",
          "parameters": [
            {
              "name": "options",
              "type": "{ restoreFocus?: boolean; reason?: string }",
              "optional": true
            }
          ],
          "returns": "void"
        }
      },
      "commands": {
        "--open": {
          "description": "Opens the dialog modally. Focus returns to the invoking button when it closes."
        },
        "--close": {
          "description": "Requests that the dialog close, with reason \"command\"; aihio-before-close can still cancel it."
        },
        "--toggle": {
          "description": "Opens the dialog when it is closed and closes it when it is open."
        }
      },
      "events": {
        "aihio-open": {
          "description": "Fired after the dialog opens.",
          "bubbles": true,
          "composed": true
        },
        "aihio-close": {
          "description": "Fired after the dialog closes.",
          "detail": {
            "reason": "string"
          },
          "bubbles": true,
          "composed": true
        },
        "aihio-before-close": {
          "description": "Cancelable request fired before the dialog closes.",
          "detail": {
            "reason": "string"
          },
          "cancelable": true,
          "bubbles": true,
          "composed": true
        }
      },
      "composition": {
        "allowedChildren": [
          "aihio-dialog-header",
          "aihio-dialog-footer",
          "#flow"
        ]
      },
      "a11yContract": {
        "handled": [
          "The native <dialog> panel supplies dialog semantics, and showModal() supplies modality",
          "aria-labelledby is wired to aihio-dialog-title when present",
          "aria-describedby is wired to aihio-dialog-description when present",
          "Native modal dialog behavior makes the document outside the dialog inert and contains keyboard focus",
          "A trigger and a Cancel button work from markup alone: commandfor plus command=\"--open\" or command=\"--close\" needs no script",
          "Opening focuses an authored autofocus element, otherwise the first rendered tab stop — never a hidden field or an unselected tab — and Tab wraps between the first and last rendered tab stops",
          "Escape closes the dialog",
          "Focus is restored to the trigger on close",
          "A shared overlay lock prevents concurrent dialogs from restoring page scroll too early"
        ],
        "required": [
          {
            "when": "dialog has no aihio-dialog-title",
            "requirement": "Provide aria-label on aihio-dialog describing the dialog's purpose; otherwise screen readers announce an unnamed dialog.",
            "severity": "error",
            "rule": "dialog-accessible-name"
          },
          {
            "when": "dialog confirms a destructive action",
            "requirement": "Label the confirming button with variant=\"destructive\" and keep Cancel as the first focusable control.",
            "severity": "warn"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-dialog open><p>Delete?</p></aihio-dialog>",
          "reason": "Missing header and footer. Provide aihio-dialog-title for a name and aihio-dialog-footer for actions.",
          "fix": "<aihio-dialog id=\"delete-project-dialog\">\n  <aihio-dialog-header>\n    <aihio-dialog-title>Delete project?</aihio-dialog-title>\n  </aihio-dialog-header>\n  <aihio-dialog-footer>\n    <aihio-button commandfor=\"delete-project-dialog\" command=\"--close\" variant=\"outline\">Cancel</aihio-button>\n    <aihio-button variant=\"destructive\">Delete</aihio-button>\n  </aihio-dialog-footer>\n</aihio-dialog>",
          "rule": "dialog-accessible-name"
        },
        {
          "markup": "<aihio-dialog><aihio-dialog><p>Nested</p></aihio-dialog></aihio-dialog>",
          "reason": "Dialogs must not be nested. Close the current dialog before opening another.",
          "fix": "<aihio-dialog id=\"first-step\" aria-label=\"Step 1\"><p>Step 1</p></aihio-dialog>\n<aihio-dialog id=\"second-step\" aria-label=\"Step 2\"><p>Step 2</p></aihio-dialog>",
          "rule": "invalid-child"
        },
        {
          "markup": "<aihio-button commandfor=\"confirm\" command=\"show-modal\">Delete</aihio-button>\n<aihio-dialog id=\"confirm\" aria-label=\"Confirm delete\"></aihio-dialog>",
          "reason": "Built-in commands such as show-modal only act on a native <dialog>, and this one is inside the component's shadow root, so nothing happens. Use command=\"--open\".",
          "fix": "<aihio-button commandfor=\"confirm\" command=\"--open\">Delete</aihio-button>\n<aihio-dialog id=\"confirm\" aria-label=\"Confirm delete\"><p>Delete this project?</p></aihio-dialog>",
          "rule": "invalid-command"
        }
      ],
      "related": [
        {
          "$component": "aihio-dialog-header",
          "description": "Dialog header container"
        },
        {
          "$component": "aihio-dialog-title",
          "description": "Dialog title text"
        },
        {
          "$component": "aihio-dialog-description",
          "description": "Dialog description text"
        },
        {
          "$component": "aihio-dialog-footer",
          "description": "Dialog footer with actions"
        }
      ],
      "examples": [
        {
          "title": "Confirm a destructive action",
          "description": "Opened and closed from markup. Submitting the form closes it.",
          "markup": "<aihio-button commandfor=\"confirm-delete\" command=\"--open\" variant=\"destructive\">Delete project</aihio-button>\n\n<aihio-dialog id=\"confirm-delete\">\n  <aihio-dialog-header>\n    <aihio-dialog-title>Are you sure?</aihio-dialog-title>\n    <aihio-dialog-description>This action cannot be undone.</aihio-dialog-description>\n  </aihio-dialog-header>\n  <form method=\"post\">\n    <aihio-dialog-footer>\n      <aihio-button commandfor=\"confirm-delete\" command=\"--close\" variant=\"outline\">Cancel</aihio-button>\n      <aihio-button type=\"submit\" variant=\"destructive\">Delete</aihio-button>\n    </aihio-dialog-footer>\n  </form>\n</aihio-dialog>"
        }
      ]
    },
    {
      "$component": "aihio-dropdown",
      "version": "1.2.0",
      "description": "A menu of actions or links opened from a trigger button, such as an account menu or a row's More actions. Not for choosing a form value: use aihio-combobox for that.",
      "intents": [
        "menu",
        "overlay",
        "dismissible"
      ],
      "attributes": {
        "open": {
          "type": "boolean",
          "default": false,
          "description": "Whether the dropdown is open"
        },
        "align": {
          "type": "enum",
          "values": [
            "start",
            "end"
          ],
          "default": "start",
          "description": "Alignment of the dropdown content relative to the trigger"
        }
      },
      "methods": {
        "open()": {
          "description": "Opens the menu in the browser top layer.",
          "parameters": [
            {
              "name": "options",
              "type": "{ focus?: \"first\" | \"last\" | null }",
              "optional": true
            }
          ],
          "returns": "void"
        },
        "close()": {
          "description": "Requests that the menu close.",
          "parameters": [
            {
              "name": "options",
              "type": "{ restoreFocus?: boolean; reason?: string }",
              "optional": true
            }
          ],
          "returns": "void"
        },
        "toggle()": {
          "description": "Toggles the menu.",
          "parameters": [
            {
              "name": "options",
              "type": "{ focus?: \"first\" | \"last\" | null; restoreFocus?: boolean }",
              "optional": true
            }
          ],
          "returns": "void"
        }
      },
      "events": {
        "aihio-open": {
          "description": "Fired after the menu opens.",
          "bubbles": true,
          "composed": true
        },
        "aihio-close": {
          "description": "Fired after the menu closes.",
          "detail": {
            "reason": "string"
          },
          "bubbles": true,
          "composed": true
        },
        "aihio-before-close": {
          "description": "Cancelable request fired before the menu closes.",
          "detail": {
            "reason": "string"
          },
          "cancelable": true,
          "bubbles": true,
          "composed": true
        }
      },
      "slots": {
        "trigger": {
          "description": "The element that opens the dropdown. Must be focusable."
        },
        "default": {
          "description": "Dropdown items and separators."
        }
      },
      "composition": {
        "requiredSlots": [
          "trigger"
        ],
        "allowedChildren": [
          "aihio-dropdown-item",
          "aihio-dropdown-separator",
          "aihio-button"
        ]
      },
      "a11yContract": {
        "handled": [
          "aria-haspopup=\"menu\" is set on the trigger",
          "aria-expanded is toggled on the trigger",
          "role=\"menu\" is set on the content panel",
          "role=\"menuitem\" is set on each dropdown item — or, when the item's child is <a href>, on that link, which also takes the focus",
          "Arrow keys move focus between items; Home/End jump to first/last",
          "Typing a character moves to the next item whose label starts with it; a repeated letter cycles through the matches",
          "A link item follows itself natively on Enter (so Ctrl/Cmd+Enter and middle-click open a new tab); Space is mapped to it",
          "Escape closes and restores focus to the trigger",
          "Tab and Shift+Tab close the menu and move on through the page",
          "Click outside closes the menu",
          "The Popover API provides top-layer rendering where supported; the component handles Escape and outside-pointer dismissal so close requests remain cancelable",
          "The menu flips above the trigger and clamps to viewport edges when needed"
        ],
        "required": [
          {
            "when": "the trigger is icon-only",
            "requirement": "Provide aria-label on the trigger (e.g. aria-label=\"Open menu\").",
            "severity": "error",
            "rule": "dropdown-trigger-name"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-dropdown>\n  <aihio-dropdown-item>Profile</aihio-dropdown-item>\n</aihio-dropdown>",
          "reason": "Missing slot=\"trigger\". The dropdown has nothing to open it.",
          "fix": "<aihio-dropdown>\n  <aihio-button slot=\"trigger\" variant=\"outline\">Account</aihio-button>\n  <aihio-dropdown-item>Profile</aihio-dropdown-item>\n</aihio-dropdown>",
          "rule": "missing-required-slot"
        },
        {
          "markup": "<aihio-dropdown>\n  <aihio-button slot=\"trigger\">Menu</aihio-button>\n  <a href=\"/x\">Link</a>\n</aihio-dropdown>",
          "reason": "Children should be aihio-dropdown-item/separator so keyboard navigation and menuitem roles work. Put the link inside an item: <aihio-dropdown-item><a href=\"/x\">Link</a></aihio-dropdown-item>.",
          "fix": "<aihio-dropdown>\n  <aihio-button slot=\"trigger\">Menu</aihio-button>\n  <aihio-dropdown-item><a href=\"/x\">Link</a></aihio-dropdown-item>\n</aihio-dropdown>",
          "rule": "invalid-child"
        }
      ],
      "related": [
        {
          "$component": "aihio-dropdown-item",
          "description": "A selectable item in the dropdown. Give it a single <a href> child to make it a link.",
          "attributes": {
            "value": {
              "type": "string",
              "description": "Item value (defaults to text content)"
            },
            "disabled": {
              "type": "boolean",
              "default": false
            }
          },
          "events": {
            "aihio-select": {
              "description": "Fired when the item is selected",
              "detail": {
                "value": "string"
              }
            }
          }
        },
        {
          "$component": "aihio-dropdown-separator",
          "description": "A visual separator between dropdown items"
        }
      ],
      "examples": [
        {
          "title": "A menu of actions",
          "markup": "<aihio-dropdown>\n  <aihio-button slot=\"trigger\" variant=\"outline\">Open</aihio-button>\n  <aihio-dropdown-item>Profile</aihio-dropdown-item>\n  <aihio-dropdown-item>Settings</aihio-dropdown-item>\n  <aihio-dropdown-separator></aihio-dropdown-separator>\n  <aihio-dropdown-item>Logout</aihio-dropdown-item>\n</aihio-dropdown>"
        },
        {
          "title": "An account menu with links",
          "description": "An item whose child is an <a href> is a real link.",
          "markup": "<aihio-dropdown align=\"end\">\n  <aihio-button slot=\"trigger\" variant=\"ghost\">Account</aihio-button>\n  <aihio-dropdown-item><a href=\"/profile\">Profile</a></aihio-dropdown-item>\n  <aihio-dropdown-item><a href=\"/settings\">Settings</a></aihio-dropdown-item>\n  <aihio-dropdown-separator></aihio-dropdown-separator>\n  <aihio-dropdown-item value=\"sign-out\">Sign out</aihio-dropdown-item>\n</aihio-dropdown>"
        }
      ]
    },
    {
      "$component": "aihio-field",
      "version": "1.3.0",
      "description": "Form field wrapper that lays out a label, control, description, and error message, and wires the accessibility relationships between them.",
      "intents": [
        "form-field",
        "layout",
        "labeling"
      ],
      "attributes": {
        "error": {
          "type": "boolean",
          "default": false,
          "description": "Reflected automatically when slot=\"error\" has content. Authors write the message; the field sets the state."
        }
      },
      "slots": {
        "label": {
          "description": "The field label. Use a <label> element to also get click-to-focus.",
          "accepts": [
            "label",
            "span",
            "#text"
          ]
        },
        "default": {
          "description": "The control itself, normally aihio-input or aihio-combobox.",
          "accepts": [
            "aihio-input",
            "aihio-combobox",
            "input",
            "select",
            "textarea"
          ]
        },
        "description": {
          "description": "Supporting hint shown under the control, hidden while an error is showing.",
          "accepts": [
            "#flow"
          ]
        },
        "error": {
          "description": "Validation message. Its presence puts the field into its error state.",
          "accepts": [
            "#flow"
          ]
        }
      },
      "composition": {
        "allowedSlots": [
          "label",
          "default",
          "description",
          "error"
        ],
        "allowedChildren": [
          "aihio-input",
          "aihio-combobox",
          "aihio-switch",
          "label",
          "span",
          "p",
          "#flow"
        ],
        "forbiddenChildren": [
          "aihio-card",
          "aihio-dialog"
        ]
      },
      "a11yContract": {
        "handled": [
          "Generates ids for the label, description, and error, and wires aria-labelledby and aria-describedby on the control",
          "Sets for on native label content and delegates label clicks to the wrapped native control",
          "Reflects the error state onto the control, which sets aria-invalid",
          "Points aria-describedby at the error message while erroring, and at the description otherwise, so only the relevant text is announced",
          "A switch field lays out as a row, the switch beside its label; a native <textarea> or <select> is drawn to match aihio-input"
        ],
        "required": [
          {
            "when": "the field has no slot=\"label\" content",
            "requirement": "Provide slot=\"label\" content, or an aria-label on the control. aihio-field wires a label but cannot invent one.",
            "severity": "error",
            "rule": "field-label"
          },
          {
            "when": "the error attribute is written on the field",
            "requirement": "Write the message in slot=\"error\" and leave the attribute to the field, which sets it from that content. An error attribute with no message marks the field invalid with nothing to announce.",
            "severity": "error",
            "rule": "field-error-message"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-field>\n  <aihio-input name=\"email\"></aihio-input>\n</aihio-field>",
          "reason": "No slot=\"label\", so the control is still unnamed. aihio-field wires a label, it does not invent one.",
          "fix": "<aihio-field>\n  <label slot=\"label\">Email</label>\n  <aihio-input name=\"email\"></aihio-input>\n</aihio-field>",
          "rule": "field-label"
        },
        {
          "markup": "<aihio-field error>\n  <span slot=\"label\">Email</span>\n  <aihio-input name=\"email\"></aihio-input>\n</aihio-field>",
          "reason": "The error attribute is set by the component from slot=\"error\" content. Writing it by hand with no message leaves nothing to announce.",
          "fix": "<aihio-field>\n  <span slot=\"label\">Email</span>\n  <aihio-input name=\"email\"></aihio-input>\n  <span slot=\"error\">Enter an email address.</span>\n</aihio-field>",
          "rule": "field-error-message"
        }
      ],
      "examples": [
        {
          "title": "With a description",
          "markup": "<aihio-field>\n  <label slot=\"label\">Email</label>\n  <aihio-input type=\"email\" name=\"email\" required></aihio-input>\n  <span slot=\"description\">Use your work address.</span>\n</aihio-field>"
        },
        {
          "title": "With an error",
          "description": "Content in slot=\"error\" puts the field in its error state and is announced with it.",
          "markup": "<aihio-field>\n  <label slot=\"label\">Password</label>\n  <aihio-input type=\"password\" name=\"password\"></aihio-input>\n  <span slot=\"error\">Password must include at least 12 characters and one symbol.</span>\n</aihio-field>"
        }
      ]
    },
    {
      "$component": "aihio-grid",
      "version": "1.0.0",
      "description": "Layout primitive for a grid of equal columns, such as a grid of cards. Columns are never narrower than 16rem and drop away as the grid narrows, so it needs no breakpoints; columns caps how many there are.",
      "intents": [
        "layout",
        "container"
      ],
      "attributes": {
        "columns": {
          "type": "enum",
          "values": [
            "2",
            "3",
            "4"
          ],
          "description": "The most columns to show. Without it, as many 16rem columns as fit."
        },
        "gap": {
          "type": "enum",
          "values": [
            "tight",
            "sm",
            "md",
            "lg"
          ],
          "default": "md",
          "description": "Spacing between rows and columns, from the spacing tokens."
        }
      },
      "slots": {
        "default": {
          "description": "The grid items, one per cell."
        }
      },
      "composition": {
        "allowedChildren": [
          "*"
        ]
      },
      "a11yContract": {
        "handled": [
          "Purely presentational: adds no role, no landmark, and no tab stop. Items keep source order, so reading and tab order match the visual order."
        ],
        "required": [
          {
            "when": "the grid is a list of like items, such as project cards",
            "requirement": "Give it list semantics when the count matters to the reader: role=\"list\" on aihio-grid and role=\"listitem\" on each item.",
            "severity": "warn"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-grid columns=\"6\"><aihio-card></aihio-card></aihio-grid>",
          "reason": "columns takes 2, 3, or 4. Six 16rem columns do not fit a typical content width, and the grid would drop them anyway.",
          "fix": "<aihio-grid columns=\"3\"><aihio-card></aihio-card></aihio-grid>",
          "rule": "invalid-enum-attribute"
        },
        {
          "markup": "<div style=\"display:grid;grid-template-columns:repeat(3,1fr);gap:16px\">…</div>",
          "reason": "A hand-rolled grid ignores the spacing scale and squeezes its columns on narrow screens. Use aihio-grid.",
          "fix": "<aihio-grid columns=\"3\">…</aihio-grid>",
          "rule": "hand-rolled-layout"
        }
      ],
      "examples": [
        {
          "title": "Three columns of cards",
          "description": "Columns drop away as the grid narrows, with no breakpoints.",
          "markup": "<aihio-grid columns=\"3\">\n  <aihio-card>\n    <aihio-card-header><aihio-card-title>Customer portal</aihio-card-title></aihio-card-header>\n  </aihio-card>\n  <aihio-card>\n    <aihio-card-header><aihio-card-title>Billing sync</aihio-card-title></aihio-card-header>\n  </aihio-card>\n  <aihio-card>\n    <aihio-card-header><aihio-card-title>Ops migration</aihio-card-title></aihio-card-header>\n  </aihio-card>\n</aihio-grid>"
        }
      ]
    },
    {
      "$component": "aihio-input",
      "version": "1.2.0",
      "description": "A single-line text field (text, email, password, number, search, and the other native types) that submits with its form. Put it inside aihio-field to get a label, description, and error message.",
      "intents": [
        "form-field",
        "text-entry"
      ],
      "attributes": {
        "type": {
          "type": "string",
          "default": "text",
          "description": "HTML input type (text, email, password, number, etc.)"
        },
        "size": {
          "type": "enum",
          "values": [
            "sm",
            "md",
            "lg"
          ],
          "default": "md",
          "description": "Input size"
        },
        "placeholder": {
          "type": "string",
          "description": "Placeholder text"
        },
        "disabled": {
          "type": "boolean",
          "default": false,
          "description": "Disables the input"
        },
        "error": {
          "type": "boolean",
          "default": false,
          "description": "Shows error styling"
        },
        "value": {
          "type": "string",
          "description": "Initial value"
        },
        "name": {
          "type": "string",
          "description": "Form field name. Required for the value to appear in FormData — without it the field submits nothing."
        },
        "required": {
          "type": "boolean",
          "default": false,
          "description": "Marks the field required for native constraint validation."
        },
        "readonly": {
          "type": "boolean",
          "default": false,
          "description": "Makes the field read-only while still submitting its value."
        },
        "autocomplete": {
          "type": "string",
          "description": "Forwarded to the inner input (e.g. email, current-password, one-time-code)."
        },
        "min": {
          "type": "string",
          "description": "Minimum value for number and date-like inputs."
        },
        "max": {
          "type": "string",
          "description": "Maximum value for number and date-like inputs."
        },
        "minlength": {
          "type": "number",
          "description": "Minimum permitted text length."
        },
        "maxlength": {
          "type": "number",
          "description": "Maximum permitted text length."
        },
        "pattern": {
          "type": "string",
          "description": "Regular expression the value must match."
        },
        "step": {
          "type": "string",
          "description": "Permitted numeric or date step."
        },
        "inputmode": {
          "type": "string",
          "description": "Hint for the virtual keyboard to display."
        },
        "enterkeyhint": {
          "type": "string",
          "description": "Hint for the virtual keyboard Enter key label."
        },
        "autocapitalize": {
          "type": "string",
          "description": "Automatic capitalization behavior."
        },
        "spellcheck": {
          "type": "boolean",
          "description": "Whether spelling and grammar checking is enabled."
        },
        "multiple": {
          "type": "boolean",
          "description": "Allows multiple values for supported input types."
        },
        "accept": {
          "type": "string",
          "description": "Accepted file types when type=file."
        },
        "capture": {
          "type": "string",
          "description": "Preferred capture source when type=file."
        },
        "list": {
          "type": "string",
          "description": "Id of a datalist providing suggestions."
        },
        "form": {
          "type": "string",
          "description": "Id of an external form that owns the input."
        }
      },
      "events": {
        "aihio-input": {
          "description": "Fired on every keystroke. The live value is available through detail and the value property; it is never copied into the value attribute.",
          "detail": {
            "value": "string"
          }
        },
        "aihio-change": {
          "description": "Fired when the value is committed (blur or Enter)",
          "detail": {
            "value": "string"
          }
        }
      },
      "properties": {
        "value": {
          "type": "string",
          "description": "Get or set the current value"
        },
        "defaultValue": {
          "type": "string",
          "description": "Get or set the reset value reflected by the value attribute"
        },
        "control": {
          "type": "HTMLInputElement | null",
          "readonly": true,
          "description": "The native input delegated to by the component"
        },
        "form": {
          "type": "HTMLFormElement | null",
          "readonly": true,
          "description": "The owning form, or null outside one"
        },
        "validity": {
          "type": "ValidityState | null",
          "readonly": true,
          "description": "Native constraint validation state"
        },
        "validationMessage": {
          "type": "string",
          "readonly": true,
          "description": "Native validation message"
        },
        "willValidate": {
          "type": "boolean",
          "readonly": true,
          "description": "Whether the control participates in constraint validation"
        }
      },
      "composition": {
        "allowedChildren": []
      },
      "a11yContract": {
        "handled": [
          "aria-invalid reflects the error attribute",
          "The inner <input> inherits placeholder, type, disabled state, constraints, and the authored default value",
          "The inner <input> is real light DOM, so name, required, readonly, autocomplete, and form ownership give native submission and validation"
        ],
        "required": [
          {
            "when": "input has no visible <label> associated by for/id",
            "requirement": "Provide aria-label on aihio-input, or wrap in a <label>. Placeholder is not a label.",
            "severity": "error",
            "rule": "input-label"
          },
          {
            "when": "error=true",
            "requirement": "Describe the error via aria-describedby pointing to a visible message; the red border alone is not conveyed to screen readers.",
            "severity": "error",
            "rule": "input-error-description"
          },
          {
            "when": "the field is inside a <form> and its value should be submitted",
            "requirement": "Set name. A field without a name is omitted from FormData entirely.",
            "severity": "warn",
            "rule": "input-form-name"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-input placeholder=\"Email\"></aihio-input>",
          "reason": "Placeholder is not a label. Associate a <label> or add aria-label.",
          "fix": "<aihio-field>\n  <label slot=\"label\">Email</label>\n  <aihio-input type=\"email\" name=\"email\" placeholder=\"you@example.com\"></aihio-input>\n</aihio-field>",
          "rule": "input-label"
        },
        {
          "markup": "<aihio-input error></aihio-input>",
          "reason": "error=true without an aria-describedby message leaves screen reader users without context.",
          "fix": "<aihio-field>\n  <label slot=\"label\">Email</label>\n  <aihio-input name=\"email\"></aihio-input>\n  <span slot=\"error\">Enter an email address.</span>\n</aihio-field>",
          "rule": "input-error-description"
        },
        {
          "markup": "<form>\n  <aihio-input aria-label=\"Email\" type=\"email\"></aihio-input>\n</form>",
          "reason": "No name attribute, so this field submits nothing. Add name=\"email\".",
          "fix": "<form>\n  <aihio-input aria-label=\"Email\" type=\"email\" name=\"email\"></aihio-input>\n</form>",
          "rule": "input-form-name"
        }
      ],
      "examples": [
        {
          "title": "Wrapped in a label",
          "markup": "<label>\n  Email\n  <aihio-input type=\"email\" placeholder=\"you@example.com\"></aihio-input>\n</label>"
        },
        {
          "title": "Named by aria-label",
          "description": "For a field with no visible label, such as a search box.",
          "markup": "<aihio-input aria-label=\"Search\" placeholder=\"Search…\"></aihio-input>"
        },
        {
          "title": "In a field, with an error",
          "markup": "<aihio-field>\n  <label slot=\"label\">Password</label>\n  <aihio-input type=\"password\" name=\"password\" autocomplete=\"new-password\"></aihio-input>\n  <span slot=\"error\">Password is too short.</span>\n</aihio-field>"
        }
      ],
      "methods": {
        "checkValidity()": {
          "description": "Runs native constraint validation and returns whether the field is valid.",
          "returns": "boolean"
        },
        "reportValidity()": {
          "description": "Runs native constraint validation and shows the browser message if invalid.",
          "returns": "boolean"
        },
        "setCustomValidity()": {
          "description": "Sets a custom validation message.",
          "parameters": [
            {
              "name": "message",
              "type": "string"
            }
          ],
          "returns": "void"
        },
        "select()": {
          "description": "Selects the input text.",
          "returns": "void"
        },
        "focus()": {
          "description": "Moves focus to the native input.",
          "parameters": [
            {
              "name": "options",
              "type": "FocusOptions",
              "optional": true
            }
          ],
          "returns": "void"
        }
      }
    },
    {
      "$component": "aihio-pagination",
      "version": "1.0.0",
      "description": "Navigation between the pages of a long list (a pager): previous and next, the first and last page, and the pages around the current one. With href each page is a link with its own address; without it each page is a button your code answers. Use it under a table, search results, or a grid of cards when the whole list is too long to show at once.",
      "intents": [
        "navigation"
      ],
      "attributes": {
        "page": {
          "type": "number",
          "default": 1,
          "description": "The current page, from 1."
        },
        "pages": {
          "type": "number",
          "description": "How many pages there are. With fewer than two there is nothing to page through, and nothing is shown."
        },
        "href": {
          "type": "string",
          "description": "The address of a page, with {page} where its number goes: \"/invoices?page={page}\". With it each page is a link that can open in a new tab and needs no script of yours. Without it each page is a button, and picking one sets page."
        },
        "previous-text": {
          "type": "string",
          "default": "Previous",
          "description": "The previous page control's text. For localisation."
        },
        "next-text": {
          "type": "string",
          "default": "Next",
          "description": "The next page control's text. For localisation."
        },
        "page-text": {
          "type": "string",
          "default": "Page {page}",
          "description": "Each page's accessible name, with {page} for its number, and what is announced when a button changes the page. For localisation (e.g. \"Sivu {page}\")."
        }
      },
      "properties": {
        "page": {
          "type": "number",
          "description": "Get or set the current page. Setting it fires no aihio-page."
        },
        "pages": {
          "type": "number",
          "description": "Get or set how many pages there are."
        }
      },
      "events": {
        "aihio-page": {
          "description": "Fired when the person picks a page, before it opens: a link then navigates, and a button sets page. Call preventDefault() to open it yourself, as a client-side router does. A click that opens a link in a new tab or window is left to the browser.",
          "detail": {
            "page": "number"
          },
          "cancelable": true,
          "bubbles": true,
          "composed": true
        }
      },
      "composition": {
        "allowedChildren": []
      },
      "a11yContract": {
        "handled": [
          "A navigation landmark, named \"Pagination\" unless you name it with aria-label or aria-labelledby",
          "The current page carries aria-current=\"page\", and is drawn with a border held to 3:1 and a heavier weight, not a colour alone",
          "Each page is named \"Page 4\" (page-text) rather than \"4\", so a list of the page's links still says what each one is",
          "Previous and next carry rel=\"prev\" and rel=\"next\". On the first and last page they stay in place, disabled and out of the tab order, so the row does not shift",
          "Past seven pages the list keeps the first and last page and the pages beside the current one, with gaps hidden from screen readers, so its length never changes as the page does. Where seven do not fit on one line, in a narrow column or on a phone, it keeps the current page alone, five slots rather than seven",
          "In a list with role=\"list\", which Safari otherwise drops once the bullets are removed",
          "A button that changes the page announces the new page in a polite status region, and focus stays on the control that was used, or moves to the current page when that control is now disabled",
          "A click that opens a link in a new tab or window is left to the browser",
          "Drawn that compactly, previous and next show only their arrows; their names do not change",
          "Forced colours: the current page is drawn in Highlight, and disabled controls in GrayText"
        ],
        "required": [
          {
            "when": "pages is missing or not a whole number, or page is outside 1 to pages",
            "requirement": "Set pages to how many pages there are, and page to the current one, counting from 1.",
            "severity": "error",
            "rule": "pagination-pages"
          },
          {
            "when": "href is set without {page}",
            "requirement": "Put {page} where the page number goes in href. Without it every page links to the same address.",
            "severity": "error",
            "rule": "pagination-href"
          },
          {
            "when": "more than one aihio-pagination is on the page",
            "requirement": "Name each one after the list it pages through, aria-label=\"Invoice pages\", so the landmarks can be told apart.",
            "severity": "warn",
            "rule": "pagination-label"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-pagination current=\"3\" total=\"12\" href=\"/invoices?page={page}\"></aihio-pagination>",
          "reason": "The current page is page and the number of pages is pages. Nothing reads current or total, so this shows nothing.",
          "fix": "<aihio-pagination page=\"3\" pages=\"12\" href=\"/invoices?page={page}\"></aihio-pagination>",
          "rule": "unknown-attribute"
        },
        {
          "markup": "<aihio-pagination page=\"3\" pages=\"12\" href=\"/invoices?page=\"></aihio-pagination>",
          "reason": "Without {page} in href every page links to the same address.",
          "fix": "<aihio-pagination page=\"3\" pages=\"12\" href=\"/invoices?page={page}\"></aihio-pagination>",
          "rule": "pagination-href"
        },
        {
          "markup": "<aihio-pagination page=\"0\" pages=\"12\" href=\"/invoices?page={page}\"></aihio-pagination>",
          "reason": "Pages count from 1. Page 0 is not a page, so no page is marked current.",
          "fix": "<aihio-pagination page=\"1\" pages=\"12\" href=\"/invoices?page={page}\"></aihio-pagination>",
          "rule": "pagination-pages"
        },
        {
          "markup": "<aihio-pagination page=\"1\" pages=\"4\" href=\"/invoices?page={page}\"></aihio-pagination>\n<aihio-pagination page=\"2\" pages=\"9\" href=\"/payments?page={page}\"></aihio-pagination>",
          "reason": "Two landmarks both named \"Pagination\" cannot be told apart in a screen reader's list of landmarks. Name each after the list it pages through.",
          "fix": "<aihio-pagination page=\"1\" pages=\"4\" href=\"/invoices?page={page}\" aria-label=\"Invoice pages\"></aihio-pagination>\n<aihio-pagination page=\"2\" pages=\"9\" href=\"/payments?page={page}\" aria-label=\"Payment pages\"></aihio-pagination>",
          "rule": "pagination-label"
        }
      ],
      "examples": [
        {
          "title": "Pages with their own address",
          "description": "With href each page is a link: it can open in a new tab or be bookmarked, and needs no script of yours.",
          "markup": "<aihio-pagination page=\"4\" pages=\"12\" href=\"/invoices?page={page}\" aria-label=\"Invoice pages\"></aihio-pagination>"
        },
        {
          "title": "Many pages",
          "description": "Past seven pages the list keeps the first and last and the pages beside the current one, and stays seven long.",
          "markup": "<aihio-pagination page=\"57\" pages=\"4000\" href=\"/requests?page={page}\" aria-label=\"Request pages\"></aihio-pagination>"
        },
        {
          "title": "Pages your code swaps in",
          "description": "Without href each page is a button. Picking one sets page and fires aihio-page with the page to show; call preventDefault() to set page yourself, from state.",
          "markup": "<aihio-pagination page=\"1\" pages=\"5\" aria-label=\"Result pages\"></aihio-pagination>"
        },
        {
          "title": "Under a table",
          "description": "The table shows one page of the records, sorted by your code or the server with manual-sort, and says which ones it shows.",
          "markup": "<aihio-stack gap=\"md\">\n  <aihio-table manual-sort>\n    <table aria-label=\"Invoices\">\n      <thead>\n        <tr>\n          <th scope=\"col\" data-sortable=\"invoice\" aria-sort=\"ascending\">Invoice</th>\n          <th scope=\"col\" data-sortable=\"amount\" data-numeric>Amount</th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr><th scope=\"row\"><a href=\"/invoices/1051\">INV-1051</a></th><td data-numeric>€480.00</td></tr>\n        <tr><th scope=\"row\"><a href=\"/invoices/1052\">INV-1052</a></th><td data-numeric>€2,150.00</td></tr>\n        <tr><th scope=\"row\"><a href=\"/invoices/1053\">INV-1053</a></th><td data-numeric>€95.20</td></tr>\n      </tbody>\n    </table>\n  </aihio-table>\n  <aihio-cluster justify=\"between\">\n    <p>Showing 51–53 of 53 invoices</p>\n    <aihio-pagination page=\"3\" pages=\"3\" href=\"/invoices?page={page}\" aria-label=\"Invoice pages\"></aihio-pagination>\n  </aihio-cluster>\n</aihio-stack>"
        },
        {
          "title": "In another language",
          "description": "previous-text, next-text, and page-text put the controls in the page's language; name the landmark in it too.",
          "markup": "<aihio-pagination page=\"2\" pages=\"6\" href=\"/laskut?sivu={page}\" aria-label=\"Laskujen sivut\" previous-text=\"Edellinen\" next-text=\"Seuraava\" page-text=\"Sivu {page}\"></aihio-pagination>"
        }
      ]
    },
    {
      "$component": "aihio-stack",
      "version": "1.0.0",
      "description": "Vertical layout primitive that applies the system's spacing rhythm between its children.",
      "intents": [
        "layout",
        "container"
      ],
      "attributes": {
        "gap": {
          "type": "enum",
          "values": [
            "tight",
            "sm",
            "md",
            "lg"
          ],
          "default": "md",
          "description": "Vertical spacing between children, from the spacing tokens."
        },
        "align": {
          "type": "enum",
          "values": [
            "start",
            "center",
            "end",
            "stretch"
          ],
          "default": "stretch",
          "description": "Cross-axis alignment of children."
        }
      },
      "slots": {
        "default": {
          "description": "Any flow content to stack vertically."
        }
      },
      "composition": {
        "allowedChildren": [
          "*"
        ]
      },
      "a11yContract": {
        "handled": [
          "Purely presentational: adds no role, no landmark, and no tab stop, so the semantics of its children are untouched."
        ],
        "required": [
          {
            "when": "the stack groups a titled region of the page",
            "requirement": "Use a semantic sectioning element (<section>, <nav>, <main>) around or inside the stack. aihio-stack does not create a landmark.",
            "severity": "warn"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-stack gap=\"medium\"><p>One</p></aihio-stack>",
          "reason": "gap=\"medium\" is not valid. The scale is tight, sm, md, lg.",
          "fix": "<aihio-stack gap=\"md\"><p>One</p></aihio-stack>",
          "rule": "invalid-enum-attribute"
        },
        {
          "markup": "<div style=\"display:flex;flex-direction:column;gap:16px\"><p>One</p></div>",
          "reason": "Hand-rolled spacing drifts from the token scale. Use aihio-stack so the rhythm stays system-owned.",
          "fix": "<aihio-stack gap=\"md\"><p>One</p></aihio-stack>",
          "rule": "hand-rolled-layout"
        }
      ],
      "examples": [
        {
          "title": "A heading and its copy",
          "markup": "<aihio-stack>\n  <h2>Notifications</h2>\n  <p>Choose how you want to hear from us.</p>\n</aihio-stack>"
        },
        {
          "title": "Centered, with room between",
          "markup": "<aihio-stack gap=\"lg\" align=\"center\">\n  <aihio-avatar alt=\"Jane Doe\"></aihio-avatar>\n  <aihio-button>Follow</aihio-button>\n</aihio-stack>"
        }
      ]
    },
    {
      "$component": "aihio-switch",
      "version": "1.0.0",
      "description": "An on/off setting that submits with its form, such as Email notifications on a settings page. Renders a real <input type=\"checkbox\" role=\"switch\">. Put it inside aihio-field for a label and description; for a pressed state in a toolbar (Bold, Italic) use aihio-toggle instead.",
      "intents": [
        "form-field",
        "toggle-state"
      ],
      "attributes": {
        "checked": {
          "type": "boolean",
          "default": false,
          "description": "Whether the switch starts on. Like a native checkbox's checked attribute, this is the reset default; the checked property is live state."
        },
        "disabled": {
          "type": "boolean",
          "default": false,
          "description": "Disables the switch"
        },
        "required": {
          "type": "boolean",
          "default": false,
          "description": "The form cannot submit until the switch is on"
        },
        "name": {
          "type": "string",
          "description": "Form field name. The switch submits name=value when on and nothing when off."
        },
        "value": {
          "type": "string",
          "default": "on",
          "description": "Value submitted when the switch is on"
        },
        "form": {
          "type": "string",
          "description": "Id of an external form that owns the switch"
        }
      },
      "events": {
        "aihio-change": {
          "description": "Fired when the user turns the switch on or off. The native change and input events also bubble from the inner checkbox.",
          "detail": {
            "checked": "boolean"
          }
        }
      },
      "properties": {
        "checked": {
          "type": "boolean",
          "description": "Live on/off state"
        },
        "defaultChecked": {
          "type": "boolean",
          "description": "Reset state; reflects the checked attribute"
        },
        "control": {
          "type": "HTMLInputElement | null",
          "readonly": true,
          "description": "The native checkbox the component delegates to"
        },
        "form": {
          "type": "HTMLFormElement | null",
          "readonly": true,
          "description": "The owning form, or null outside one"
        },
        "validity": {
          "type": "ValidityState | null",
          "readonly": true,
          "description": "Native constraint validation state"
        },
        "validationMessage": {
          "type": "string",
          "readonly": true,
          "description": "Native validation message"
        },
        "willValidate": {
          "type": "boolean",
          "readonly": true,
          "description": "Whether the switch takes part in constraint validation"
        }
      },
      "methods": {
        "click()": {
          "description": "Flips the switch, as a click on it would.",
          "returns": "void"
        },
        "focus()": {
          "description": "Moves focus to the native checkbox.",
          "parameters": [
            {
              "name": "options",
              "type": "FocusOptions",
              "optional": true
            }
          ],
          "returns": "void"
        },
        "checkValidity()": {
          "description": "Runs native constraint validation.",
          "returns": "boolean"
        },
        "reportValidity()": {
          "description": "Runs native constraint validation and shows the browser's message.",
          "returns": "boolean"
        },
        "setCustomValidity()": {
          "description": "Sets a custom validation message; an empty string clears it.",
          "parameters": [
            {
              "name": "message",
              "type": "string"
            }
          ],
          "returns": "void"
        }
      },
      "composition": {
        "allowedChildren": []
      },
      "a11yContract": {
        "handled": [
          "The component renders a real <input type=\"checkbox\" role=\"switch\">, so Space toggles it, it is announced as a switch that is on or off, and focus and disabled semantics are native",
          "Form participation is native: it submits name=value when on, resets with its form, and <fieldset disabled> disables it",
          "aria-label, aria-labelledby, aria-describedby, and aria-invalid set on the host are forwarded to the checkbox",
          "Inside aihio-field the label, description, and error are wired to the checkbox, and the field lays out as a row with the switch beside its label",
          "On and off stay distinguishable under forced colours: the track keeps a border and the on state uses Highlight"
        ],
        "required": [
          {
            "when": "the switch has no label",
            "requirement": "Name the switch after the setting it controls: put it in aihio-field with a <label slot=\"label\">, wrap it in a <label>, or give it aria-label. Never label it with its state (\"On\", \"Enabled\").",
            "severity": "error",
            "rule": "switch-label"
          },
          {
            "when": "the switch is inside a <form> and its state should be submitted",
            "requirement": "Set name. A switch without a name is omitted from FormData entirely.",
            "severity": "warn",
            "rule": "switch-form-name"
          },
          {
            "when": "the switch is named by a state word (On, Off, Enabled)",
            "requirement": "Name the switch after the setting it controls. The switch announces its own state, so a switch named \"On\" is read as \"On, switch, on\" and never says what is on.",
            "severity": "warn",
            "rule": "switch-state-name"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<span>Email notifications</span>\n<aihio-switch name=\"email-notifications\"></aihio-switch>",
          "reason": "The visible text is not associated with the switch, so it is announced as an unnamed switch. Use aihio-field with <label slot=\"label\">, or wrap both in a <label>.",
          "fix": "<aihio-field>\n  <label slot=\"label\">Email notifications</label>\n  <aihio-switch name=\"email-notifications\"></aihio-switch>\n</aihio-field>",
          "rule": "switch-label"
        },
        {
          "markup": "<aihio-switch aria-label=\"On\" checked></aihio-switch>",
          "reason": "A switch named by its state is announced as \"On, switch, on\". Name it after the setting it controls.",
          "fix": "<aihio-switch aria-label=\"Email notifications\" checked></aihio-switch>",
          "rule": "switch-state-name"
        },
        {
          "markup": "<aihio-switch checked=\"false\" aria-label=\"Weekly summary\"></aihio-switch>",
          "reason": "Boolean attributes are on by presence. checked=\"false\" starts the switch on; omit the attribute for off.",
          "fix": "<aihio-switch aria-label=\"Weekly summary\"></aihio-switch>",
          "rule": "boolean-attribute-value"
        }
      ],
      "examples": [
        {
          "title": "In a field",
          "description": "Laid out as a row beside its label, with a description beneath.",
          "markup": "<aihio-field>\n  <label slot=\"label\">Email notifications</label>\n  <aihio-switch name=\"email-notifications\" checked></aihio-switch>\n  <span slot=\"description\">A digest of activity, sent weekly.</span>\n</aihio-field>"
        },
        {
          "title": "Wrapped in a label",
          "markup": "<label>\n  <aihio-switch name=\"remember\"></aihio-switch>\n  Remember this device\n</label>"
        }
      ]
    },
    {
      "$component": "aihio-table",
      "version": "1.0.0",
      "description": "A data table: records in rows and columns, compared by scanning down a column. It wraps a native <table> and gives it the system's styles, sortable columns, and a scroll box the keyboard can reach when the table is too wide for the screen. Use it for records people compare (invoices, members, deployments); use aihio-grid of cards for a few summaries, and never a table for layout. For more rows than a page can hold, show one page at a time with aihio-pagination, or use aihio-data-grid.",
      "intents": [
        "tabular-data"
      ],
      "attributes": {
        "density": {
          "type": "enum",
          "values": [
            "default",
            "compact"
          ],
          "default": "default",
          "description": "Row spacing. compact fits more rows on screen, for dense data that people scan."
        },
        "sticky-header": {
          "type": "boolean",
          "default": false,
          "description": "Keeps the header in view while the rows scroll. The table scrolls inside its own box, which grows to --aihio-table-max-height (min(32rem, 75dvh) unless you set it) before its rows scroll."
        },
        "manual-sort": {
          "type": "boolean",
          "default": false,
          "description": "Leaves the order of the rows to your code. A click on a sortable header still marks it with aria-sort, announces it, and fires aihio-sort, but the rows stay as rendered: sort them yourself, as a framework re-rendering from state or a request to the server does. Without it the table reorders its own rows."
        },
        "loading": {
          "type": "boolean",
          "default": false,
          "description": "Marks the rows as stale while new ones load, such as after a sort sent to the server: they are dimmed, and the table has aria-busy."
        },
        "sort-ascending-text": {
          "type": "string",
          "default": "Sorted by {column}, ascending",
          "description": "Announced after sorting a column ascending, with {column} for the header's text. For localisation (e.g. \"Lajiteltu: {column}, nouseva\")."
        },
        "sort-descending-text": {
          "type": "string",
          "default": "Sorted by {column}, descending",
          "description": "Announced after sorting a column descending, with {column} for the header's text."
        }
      },
      "nativeElements": {
        "th": {
          "description": "A header cell. In <thead> it names a column; in a body row, with scope=\"row\", it names the row.",
          "attributes": {
            "data-sortable": {
              "type": "string",
              "description": "Makes the column sortable: the header's content becomes a button that sorts the rows by it, ascending first and then descending. The value names the column for aihio-sort and sort(), and can be left empty when nothing reads it. Only on a <th> in <thead>."
            },
            "aria-sort": {
              "type": "enum",
              "values": [
                "ascending",
                "descending",
                "none",
                "other"
              ],
              "description": "The order the rows are in. Write it on the header the rows are sorted by when the page loads; without manual-sort the table puts the rows in that order. The table moves it from header to header as people sort."
            },
            "data-numeric": {
              "type": "boolean",
              "default": false,
              "description": "Lines a column of figures up by place value: end-aligned, in tabular digits. Put it on the column's header and on each of its cells."
            },
            "data-sort-value": {
              "type": "string",
              "description": "What a row header sorts by, when its text does not sort the way it reads. See <td>."
            }
          }
        },
        "td": {
          "description": "A data cell.",
          "attributes": {
            "data-sort-value": {
              "type": "string",
              "description": "What the cell sorts by when its text does not sort the way it reads, such as a priority (\"High\" before \"Low\") or a date written out. A <time datetime> in the cell is read the same way, so a date marked up as one needs nothing more. Figures written for the page's language (\"€1,250.50\") are read as numbers without it."
            },
            "data-numeric": {
              "type": "boolean",
              "default": false,
              "description": "Lines the figure up with the rest of its column. See <th>."
            }
          }
        }
      },
      "properties": {
        "table": {
          "type": "HTMLTableElement | null",
          "readonly": true,
          "description": "The <table> the element enhances."
        }
      },
      "methods": {
        "sort()": {
          "description": "Sorts by the column whose data-sortable value, or header text, is column: ascending unless direction says otherwise. It moves aria-sort to that header and, without manual-sort, reorders the rows. Like a value set from script, it fires no aihio-sort and announces nothing.",
          "parameters": [
            {
              "name": "column",
              "type": "string"
            },
            {
              "name": "direction",
              "type": "'ascending' | 'descending'",
              "optional": true
            }
          ],
          "returns": "void"
        }
      },
      "events": {
        "aihio-sort": {
          "description": "Fired when the person sorts by a column, once its header carries the new aria-sort and, without manual-sort, the rows are in their new order. column is the header's data-sortable value, or its text when that is empty. Not fired by sort().",
          "detail": {
            "column": "string",
            "direction": "'ascending' | 'descending'"
          },
          "bubbles": true,
          "composed": true
        }
      },
      "composition": {
        "requiredChildren": [
          "table"
        ],
        "allowedChildren": [
          "table"
        ]
      },
      "a11yContract": {
        "handled": [
          "It stays a native <table>: rows, columns, and header cells keep the platform's table semantics, so screen reader table navigation names the column and row of every cell, and nothing is re-created with ARIA roles",
          "A sortable header's content moves into a <button> inside the <th>, which carries aria-sort, so the header keeps its role and the button sorts; the new order is announced in a polite status region",
          "The first activation sorts ascending and the next descending; rows with no value go last either way, ties keep their order, a column of figures compares as numbers (read the way the page's language writes them), and text compares in the page's language with digits in numeric order (\"Item 2\" before \"Item 10\")",
          "Rows are moved with moveBefore() where the browser has it, so focus and state inside a row survive a sort; elsewhere focus is put back after the move",
          "A table wider than its box (or, with sticky-header, taller) scrolls inside it, and the box becomes a tab stop and a region named by the caption, so the keyboard can scroll it and a screen reader says what it is; when it fits again it is neither",
          "The sort button's focus ring is drawn inside the header cell, so the scroll box never clips it",
          "loading sets aria-busy on the table while the rows are stale",
          "Sorting is by a button, not a click handler on the cell, and the sort indicator changes shape, not only colour; under forced colours the sort buttons are drawn as system buttons"
        ],
        "required": [
          {
            "when": "the table has no caption, aria-label, or aria-labelledby",
            "requirement": "Name the table: a <caption> as its first child, aria-labelledby pointing at the visible heading above it, or aria-label. An unnamed table is announced only as \"table\", and so is the scroll box it gets on a narrow screen.",
            "severity": "error",
            "rule": "table-accessible-name"
          },
          {
            "when": "the table has no <th> cells",
            "requirement": "Name each column with a <th> in a <thead> row, and each row with <th scope=\"row\"> where one cell identifies it. Bold text in an ordinary cell is not a header.",
            "severity": "error",
            "rule": "table-header-cells"
          },
          {
            "when": "a row, header, or cell responds to clicks and holds no link or button",
            "requirement": "Sort with data-sortable on the <th>, and put a link or button in the row for anything else, such as a link in the cell that names it. A click handler on a row or cell is reachable by pointer only.",
            "severity": "error",
            "rule": "table-click-handler"
          },
          {
            "when": "manual-sort is set and a sortable header has no column name",
            "requirement": "Give each sortable header a name, data-sortable=\"amount\", so aihio-sort says which column to sort by without depending on the header's wording.",
            "severity": "warn",
            "rule": "table-sort-column-name"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-table>\n  <table>\n    <thead>\n      <tr>\n        <th scope=\"col\">Invoice</th>\n        <th scope=\"col\" data-numeric>Amount</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <th scope=\"row\">INV-1042</th>\n        <td data-numeric>€1,250.00</td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>",
          "reason": "An unnamed table is announced only as \"table\". On a narrow screen it also scrolls inside a box with no name. Give it a <caption>, or point aria-labelledby at the heading above it.",
          "fix": "<aihio-table>\n  <table>\n    <caption>Invoices</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\">Invoice</th>\n        <th scope=\"col\" data-numeric>Amount</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <th scope=\"row\">INV-1042</th>\n        <td data-numeric>€1,250.00</td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>",
          "rule": "table-accessible-name"
        },
        {
          "markup": "<aihio-table>\n  <table>\n    <caption>Invoices</caption>\n    <tr>\n      <td><strong>Invoice</strong></td>\n      <td><strong>Amount</strong></td>\n    </tr>\n    <tr>\n      <td>INV-1042</td>\n      <td>€1,250.00</td>\n    </tr>\n  </table>\n</aihio-table>",
          "reason": "Bold text in an ordinary cell looks like a header but is not one. A screen reader moving down a column cannot say which column it is in. Put the column names in <th> cells in a <thead>.",
          "fix": "<aihio-table>\n  <table>\n    <caption>Invoices</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\">Invoice</th>\n        <th scope=\"col\" data-numeric>Amount</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <th scope=\"row\">INV-1042</th>\n        <td data-numeric>€1,250.00</td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>",
          "rule": "table-header-cells"
        },
        {
          "markup": "<aihio-table>\n  <table>\n    <caption>Invoices</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\" onclick=\"sortBy('invoice')\">Invoice</th>\n        <th scope=\"col\" onclick=\"sortBy('amount')\" data-numeric>Amount</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <th scope=\"row\">INV-1042</th>\n        <td data-numeric>€1,250.00</td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>",
          "reason": "A header that sorts on click cannot be reached with the keyboard, and never says which way it sorts. data-sortable gives the header a real button, sets aria-sort, and announces the new order.",
          "fix": "<aihio-table>\n  <table>\n    <caption>Invoices</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable=\"invoice\">Invoice</th>\n        <th scope=\"col\" data-sortable=\"amount\" data-numeric>Amount</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <th scope=\"row\">INV-1042</th>\n        <td data-numeric>€1,250.00</td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>",
          "rule": "table-click-handler"
        },
        {
          "markup": "<aihio-table>\n  <table>\n    <caption>Invoices</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\">Invoice</th>\n        <th scope=\"col\" data-numeric>Amount</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr onclick=\"location.href = '/invoices/1042'\">\n        <th scope=\"row\">INV-1042</th>\n        <td data-numeric>€1,250.00</td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>",
          "reason": "A row that opens on click is not a link. The keyboard cannot reach it, a screen reader does not announce it, and it cannot be opened in a new tab. Link the cell that names the row.",
          "fix": "<aihio-table>\n  <table>\n    <caption>Invoices</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\">Invoice</th>\n        <th scope=\"col\" data-numeric>Amount</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <th scope=\"row\"><a href=\"/invoices/1042\">INV-1042</a></th>\n        <td data-numeric>€1,250.00</td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>",
          "rule": "table-click-handler"
        },
        {
          "markup": "<aihio-table>\n  <aihio-table-header>\n    <aihio-table-row>\n      <aihio-table-head>Invoice</aihio-table-head>\n    </aihio-table-row>\n  </aihio-table-header>\n  <aihio-table-body>\n    <aihio-table-row>\n      <aihio-table-cell>INV-1042</aihio-table-cell>\n    </aihio-table-row>\n  </aihio-table-body>\n</aihio-table>",
          "reason": "There are no row or cell components. aihio-table wraps a native <table>, whose rows and cells are what a screen reader navigates and what the HTML parser keeps in order.",
          "fix": "<aihio-table>\n  <table>\n    <caption>Invoices</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\">Invoice</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <td>INV-1042</td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>",
          "rule": "unknown-component"
        },
        {
          "markup": "<aihio-table>\n  <table>\n    <caption>Invoices</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\" sortable>Invoice</th>\n        <th scope=\"col\" sortable data-numeric>Amount</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <th scope=\"row\">INV-1042</th>\n        <td data-numeric>€1,250.00</td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>",
          "reason": "Nothing reads a sortable attribute on a <th>. The table reads data-sortable, and makes that header a sort button.",
          "fix": "<aihio-table>\n  <table>\n    <caption>Invoices</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable>Invoice</th>\n        <th scope=\"col\" data-sortable data-numeric>Amount</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <th scope=\"row\">INV-1042</th>\n        <td data-numeric>€1,250.00</td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>",
          "rule": "table-sortable-header"
        }
      ],
      "examples": [
        {
          "title": "Records with a caption",
          "description": "Each column is named by a <th> in <thead>, and the cell that names each row is a <th scope=\"row\">. data-numeric lines figures up by place value, and a <tfoot> holds the totals.",
          "markup": "<aihio-table>\n  <table>\n    <caption>Invoices</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\">Invoice</th>\n        <th scope=\"col\">Customer</th>\n        <th scope=\"col\">Status</th>\n        <th scope=\"col\" data-numeric>Amount</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <th scope=\"row\">INV-1042</th>\n        <td>Northwind Traders</td>\n        <td><aihio-badge variant=\"success\">Paid</aihio-badge></td>\n        <td data-numeric>€1,250.00</td>\n      </tr>\n      <tr>\n        <th scope=\"row\">INV-1043</th>\n        <td>Fabrikam</td>\n        <td><aihio-badge variant=\"warning\">Due</aihio-badge></td>\n        <td data-numeric>€980.50</td>\n      </tr>\n      <tr>\n        <th scope=\"row\">INV-1044</th>\n        <td>Contoso</td>\n        <td><aihio-badge variant=\"destructive\">Overdue</aihio-badge></td>\n        <td data-numeric>€12,400.00</td>\n      </tr>\n    </tbody>\n    <tfoot>\n      <tr>\n        <th scope=\"row\" colspan=\"3\">Total</th>\n        <td data-numeric>€14,630.50</td>\n      </tr>\n    </tfoot>\n  </table>\n</aihio-table>"
        },
        {
          "title": "Sortable columns",
          "description": "data-sortable makes a header a sort button. The rows load sorted by the header with aria-sort. A <time datetime> sorts by its machine-readable value, and data-sort-value gives a priority an order its words do not have.",
          "markup": "<aihio-table>\n  <table>\n    <caption>Deployments</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable=\"service\">Service</th>\n        <th scope=\"col\" data-sortable=\"priority\">Priority</th>\n        <th scope=\"col\" data-sortable=\"deployed\" aria-sort=\"descending\">Deployed</th>\n        <th scope=\"col\" data-sortable=\"duration\" data-numeric>Duration (s)</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <th scope=\"row\">api-gateway</th>\n        <td data-sort-value=\"1\">High</td>\n        <td><time datetime=\"2026-10-05T14:20\">5 Oct, 14:20</time></td>\n        <td data-numeric>42.5</td>\n      </tr>\n      <tr>\n        <th scope=\"row\">billing-worker</th>\n        <td data-sort-value=\"3\">Low</td>\n        <td><time datetime=\"2026-10-04T09:05\">4 Oct, 09:05</time></td>\n        <td data-numeric>118.0</td>\n      </tr>\n      <tr>\n        <th scope=\"row\">search-indexer</th>\n        <td data-sort-value=\"2\">Medium</td>\n        <td><time datetime=\"2026-09-29T17:45\">29 Sep, 17:45</time></td>\n        <td data-numeric>7.25</td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>"
        },
        {
          "title": "Compact, with a sticky header",
          "description": "density=\"compact\" fits more rows on screen, and sticky-header keeps the column names in view as they scroll. --aihio-table-max-height sets how tall the box grows first.",
          "markup": "<aihio-table density=\"compact\" sticky-header style=\"--aihio-table-max-height: 16rem\">\n  <table>\n    <caption>Requests</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\">Time</th>\n        <th scope=\"col\">Method</th>\n        <th scope=\"col\">Path</th>\n        <th scope=\"col\" data-numeric>Status</th>\n        <th scope=\"col\" data-numeric>Duration (ms)</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <td><time datetime=\"2026-10-06T09:41:02\">09:41:02</time></td>\n        <td>GET</td>\n        <td>/api/projects</td>\n        <td data-numeric>200</td>\n        <td data-numeric>38</td>\n      </tr>\n      <tr>\n        <td><time datetime=\"2026-10-06T09:41:03\">09:41:03</time></td>\n        <td>GET</td>\n        <td>/api/projects/42</td>\n        <td data-numeric>200</td>\n        <td data-numeric>21</td>\n      </tr>\n      <tr>\n        <td><time datetime=\"2026-10-06T09:41:07\">09:41:07</time></td>\n        <td>POST</td>\n        <td>/api/deployments</td>\n        <td data-numeric>201</td>\n        <td data-numeric>412</td>\n      </tr>\n      <tr>\n        <td><time datetime=\"2026-10-06T09:41:09\">09:41:09</time></td>\n        <td>GET</td>\n        <td>/api/deployments/97</td>\n        <td data-numeric>200</td>\n        <td data-numeric>17</td>\n      </tr>\n      <tr>\n        <td><time datetime=\"2026-10-06T09:41:12\">09:41:12</time></td>\n        <td>PATCH</td>\n        <td>/api/projects/42</td>\n        <td data-numeric>200</td>\n        <td data-numeric>64</td>\n      </tr>\n      <tr>\n        <td><time datetime=\"2026-10-06T09:41:15\">09:41:15</time></td>\n        <td>GET</td>\n        <td>/api/members</td>\n        <td data-numeric>200</td>\n        <td data-numeric>29</td>\n      </tr>\n      <tr>\n        <td><time datetime=\"2026-10-06T09:41:18\">09:41:18</time></td>\n        <td>DELETE</td>\n        <td>/api/tokens/7</td>\n        <td data-numeric>204</td>\n        <td data-numeric>33</td>\n      </tr>\n      <tr>\n        <td><time datetime=\"2026-10-06T09:41:21\">09:41:21</time></td>\n        <td>GET</td>\n        <td>/api/billing</td>\n        <td data-numeric>503</td>\n        <td data-numeric>1204</td>\n      </tr>\n      <tr>\n        <td><time datetime=\"2026-10-06T09:41:22\">09:41:22</time></td>\n        <td>GET</td>\n        <td>/api/billing</td>\n        <td data-numeric>200</td>\n        <td data-numeric>88</td>\n      </tr>\n      <tr>\n        <td><time datetime=\"2026-10-06T09:41:30\">09:41:30</time></td>\n        <td>POST</td>\n        <td>/api/invites</td>\n        <td data-numeric>422</td>\n        <td data-numeric>45</td>\n      </tr>\n      <tr>\n        <td><time datetime=\"2026-10-06T09:41:31\">09:41:31</time></td>\n        <td>POST</td>\n        <td>/api/invites</td>\n        <td data-numeric>201</td>\n        <td data-numeric>97</td>\n      </tr>\n      <tr>\n        <td><time datetime=\"2026-10-06T09:41:36\">09:41:36</time></td>\n        <td>GET</td>\n        <td>/api/audit</td>\n        <td data-numeric>200</td>\n        <td data-numeric>143</td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>"
        },
        {
          "title": "Sorted by your code",
          "description": "manual-sort leaves the rows to you: listen for aihio-sort and render them sorted by event.detail.column and event.detail.direction, as a framework re-rendering from state or a request to the server does. The table still marks the header and announces the sort.",
          "markup": "<aihio-table manual-sort>\n  <table>\n    <caption>Customers</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\" data-sortable=\"name\" aria-sort=\"ascending\">Name</th>\n        <th scope=\"col\" data-sortable=\"plan\">Plan</th>\n        <th scope=\"col\" data-sortable=\"seats\" data-numeric>Seats</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <th scope=\"row\">Contoso</th>\n        <td>Business</td>\n        <td data-numeric>120</td>\n      </tr>\n      <tr>\n        <th scope=\"row\">Fabrikam</th>\n        <td>Team</td>\n        <td data-numeric>18</td>\n      </tr>\n      <tr>\n        <th scope=\"row\">Northwind Traders</th>\n        <td>Enterprise</td>\n        <td data-numeric>740</td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>"
        },
        {
          "title": "Links and row actions",
          "description": "The cell that names each row links to it, and each row's menu is named for its row, so a screen reader can tell the menus apart.",
          "markup": "<aihio-table>\n  <table>\n    <caption>Team members</caption>\n    <thead>\n      <tr>\n        <th scope=\"col\">Name</th>\n        <th scope=\"col\">Role</th>\n        <th scope=\"col\">Actions</th>\n      </tr>\n    </thead>\n    <tbody>\n      <tr>\n        <th scope=\"row\"><a href=\"/team/ada\">Ada Lovelace</a></th>\n        <td>Owner</td>\n        <td>\n          <aihio-dropdown align=\"end\">\n            <aihio-button slot=\"trigger\" variant=\"ghost\" size=\"icon\" aria-label=\"Actions for Ada Lovelace\">&#x22ef;</aihio-button>\n            <aihio-dropdown-item>Change role</aihio-dropdown-item>\n            <aihio-dropdown-item>Remove from team</aihio-dropdown-item>\n          </aihio-dropdown>\n        </td>\n      </tr>\n      <tr>\n        <th scope=\"row\"><a href=\"/team/grace\">Grace Hopper</a></th>\n        <td>Admin</td>\n        <td>\n          <aihio-dropdown align=\"end\">\n            <aihio-button slot=\"trigger\" variant=\"ghost\" size=\"icon\" aria-label=\"Actions for Grace Hopper\">&#x22ef;</aihio-button>\n            <aihio-dropdown-item>Change role</aihio-dropdown-item>\n            <aihio-dropdown-item>Remove from team</aihio-dropdown-item>\n          </aihio-dropdown>\n        </td>\n      </tr>\n    </tbody>\n  </table>\n</aihio-table>"
        }
      ]
    },
    {
      "$component": "aihio-tabs",
      "version": "1.1.0",
      "description": "Switches between sibling panels of content with a row of tabs, such as the sections of a settings page. Arrow keys move between tabs.",
      "intents": [
        "tabs",
        "navigation",
        "layout"
      ],
      "attributes": {
        "value": {
          "type": "string",
          "description": "The currently active tab value"
        }
      },
      "composition": {
        "requiredChildren": [
          "aihio-tab-list",
          "aihio-tab-panel"
        ],
        "allowedChildren": [
          "aihio-tab-list",
          "aihio-tab-panel"
        ]
      },
      "a11yContract": {
        "handled": [
          "role=\"tablist\" on aihio-tab-list",
          "role=\"tab\" on aihio-tab with aria-selected and tabindex roving",
          "role=\"tabpanel\" on aihio-tab-panel with aria-labelledby wired to its tab",
          "Arrow keys, Home, End move between tabs",
          "Panels are hidden when inactive"
        ],
        "required": [
          {
            "when": "every aihio-tab and aihio-tab-panel",
            "requirement": "Match each tab's value to exactly one panel's value. Mismatched values leave panels orphaned.",
            "severity": "error",
            "rule": "tabs-value-pairs"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-tabs>\n  <aihio-tab value=\"a\">A</aihio-tab>\n  <aihio-tab-panel value=\"a\">Content</aihio-tab-panel>\n</aihio-tabs>",
          "reason": "aihio-tab must sit inside an aihio-tab-list; keyboard navigation is bound to the list.",
          "fix": "<aihio-tabs value=\"a\">\n  <aihio-tab-list>\n    <aihio-tab value=\"a\">A</aihio-tab>\n  </aihio-tab-list>\n  <aihio-tab-panel value=\"a\">Content</aihio-tab-panel>\n</aihio-tabs>",
          "rule": "missing-required-child"
        },
        {
          "markup": "<aihio-tabs value=\"a\">\n  <aihio-tab-list>\n    <aihio-tab value=\"a\">A</aihio-tab>\n  </aihio-tab-list>\n  <aihio-tab-panel value=\"b\">Content</aihio-tab-panel>\n</aihio-tabs>",
          "reason": "Panel value does not match any tab value; the panel will never activate.",
          "fix": "<aihio-tabs value=\"a\">\n  <aihio-tab-list>\n    <aihio-tab value=\"a\">A</aihio-tab>\n  </aihio-tab-list>\n  <aihio-tab-panel value=\"a\">Content</aihio-tab-panel>\n</aihio-tabs>",
          "rule": "tabs-value-pairs"
        }
      ],
      "related": [
        {
          "$component": "aihio-tab-list",
          "description": "Container for tab triggers"
        },
        {
          "$component": "aihio-tab",
          "description": "Individual tab trigger",
          "attributes": {
            "value": {
              "type": "string",
              "description": "Stable value that matches a corresponding aihio-tab-panel."
            },
            "disabled": {
              "type": "boolean",
              "default": false,
              "description": "Disables the tab and removes it from keyboard navigation."
            }
          },
          "events": {
            "aihio-tab-select": {
              "description": "Fired when the tab is activated; aihio-tabs listens for this to drive selection",
              "detail": {
                "value": "string"
              }
            }
          }
        },
        {
          "$component": "aihio-tab-panel",
          "description": "Content panel for a tab",
          "attributes": {
            "value": {
              "type": "string",
              "description": "Stable value that matches a corresponding aihio-tab."
            }
          }
        }
      ],
      "examples": [
        {
          "title": "Two panels",
          "markup": "<aihio-tabs value=\"one\">\n  <aihio-tab-list>\n    <aihio-tab value=\"one\">Tab 1</aihio-tab>\n    <aihio-tab value=\"two\">Tab 2</aihio-tab>\n  </aihio-tab-list>\n  <aihio-tab-panel value=\"one\">Content 1</aihio-tab-panel>\n  <aihio-tab-panel value=\"two\">Content 2</aihio-tab-panel>\n</aihio-tabs>"
        }
      ]
    },
    {
      "$component": "aihio-toggle",
      "version": "2.0.0",
      "description": "A button that stays pressed or unpressed, such as Bold in a text toolbar. It renders a real <button aria-pressed>.",
      "intents": [
        "toggle-state",
        "action"
      ],
      "attributes": {
        "pressed": {
          "type": "boolean",
          "default": false,
          "description": "Whether the toggle is pressed"
        },
        "disabled": {
          "type": "boolean",
          "default": false,
          "description": "Disables the toggle"
        },
        "variant": {
          "type": "enum",
          "values": [
            "default",
            "outline"
          ],
          "default": "default",
          "description": "Visual style variant"
        },
        "size": {
          "type": "enum",
          "values": [
            "sm",
            "md",
            "lg"
          ],
          "default": "md",
          "description": "Toggle size"
        }
      },
      "slots": {
        "default": {
          "description": "Toggle label content"
        }
      },
      "events": {
        "aihio-toggle": {
          "description": "Fired when the pressed state changes",
          "detail": {
            "pressed": "boolean"
          }
        }
      },
      "composition": {
        "forbiddenChildren": [
          "aihio-button",
          "aihio-toggle",
          "aihio-input"
        ]
      },
      "a11yContract": {
        "handled": [
          "The component renders a real <button> with aria-pressed, so the role, Enter and Space activation, focus behaviour, disabled semantics, and the forced-colours mapping are the platform's rather than an emulation of them",
          "aria-pressed on that control reflects the pressed attribute",
          "aria-* set on the host is mirrored onto the control",
          "The control is always type=\"button\", so a toggle inside a form never submits it"
        ],
        "required": [
          {
            "when": "toggle has no visible text (icon-only)",
            "requirement": "Provide aria-label describing what the toggle controls (e.g. aria-label=\"Bold\").",
            "severity": "error",
            "rule": "toggle-accessible-name"
          },
          {
            "when": "the toggle turns a setting on or off, or is named by a state word (On, Enabled)",
            "requirement": "Name the toggle after what it controls (e.g. \"Bold\"), never after its state: a toggle labelled \"Enabled\" is announced as \"Enabled, toggle button, pressed\" and never says what is enabled. For an on/off setting in a form, use aihio-switch.",
            "severity": "warn",
            "rule": "toggle-state-name"
          }
        ]
      },
      "counterExamples": [
        {
          "markup": "<aihio-toggle><aihio-button>B</aihio-button></aihio-toggle>",
          "reason": "Nested interactives. Use aihio-toggle alone — it already renders a button.",
          "fix": "<aihio-toggle aria-label=\"Bold\">B</aihio-toggle>",
          "rule": "forbidden-descendant"
        },
        {
          "markup": "<aihio-toggle pressed=\"false\">Bold</aihio-toggle>",
          "reason": "Boolean attributes toggle on presence. Omit the attribute for unpressed; write `pressed` for pressed.",
          "fix": "<aihio-toggle>Bold</aihio-toggle>",
          "rule": "boolean-attribute-value"
        },
        {
          "markup": "<span>Email notifications</span>\n<aihio-toggle pressed>Enabled</aihio-toggle>",
          "reason": "The toggle is named by its state and the visible label is not associated with it, so assistive technology announces \"Enabled, toggle button, pressed\" with no idea what is enabled. An on/off setting is a switch: use aihio-switch in aihio-field, which also submits with the form.",
          "fix": "<aihio-field>\n  <label slot=\"label\">Email notifications</label>\n  <aihio-switch name=\"email-notifications\" checked></aihio-switch>\n</aihio-field>",
          "rule": "toggle-state-name"
        }
      ],
      "examples": [
        {
          "title": "Unpressed",
          "markup": "<aihio-toggle>Bold</aihio-toggle>"
        },
        {
          "title": "Pressed and outlined",
          "description": "pressed is a boolean attribute: present means on.",
          "markup": "<aihio-toggle pressed variant=\"outline\">Italic</aihio-toggle>"
        }
      ],
      "properties": {
        "control": {
          "type": "HTMLButtonElement | null",
          "readonly": true,
          "description": "The native button delegated to by the component"
        },
        "pressed": {
          "type": "boolean",
          "description": "Whether the toggle is pressed; reflects the pressed attribute"
        }
      },
      "methods": {
        "click()": {
          "description": "Activates the native button, toggling the pressed state.",
          "returns": "void"
        },
        "focus()": {
          "description": "Moves focus to the native button.",
          "parameters": [
            {
              "name": "options",
              "type": "FocusOptions",
              "optional": true
            }
          ],
          "returns": "void"
        },
        "blur()": {
          "description": "Removes focus from the native button.",
          "returns": "void"
        }
      }
    }
  ],
  "patterns": [
    {
      "id": "auth-form",
      "name": "Auth Form",
      "intents": [
        "surface",
        "layout",
        "form-field",
        "text-entry",
        "primary-action",
        "secondary-action"
      ],
      "description": "Card-based authentication form with labelled fields and a dominant submit action inside the form element.",
      "requiredComponents": [
        "aihio-card",
        "aihio-field",
        "aihio-input",
        "aihio-cluster",
        "aihio-button"
      ],
      "markup": "<aihio-card data-aihio-intent=\"surface\">\n  <form>\n    <aihio-card-header>\n      <aihio-card-title>Sign in</aihio-card-title>\n      <aihio-card-description>Use your work email and password to continue.</aihio-card-description>\n    </aihio-card-header>\n    <aihio-card-content>\n      <aihio-stack data-aihio-intent=\"layout\">\n        <aihio-field data-aihio-intent=\"form-field\">\n          <label slot=\"label\">Email</label>\n          <aihio-input\n            data-aihio-intent=\"text-entry\"\n            type=\"email\"\n            name=\"email\"\n            autocomplete=\"email\"\n            placeholder=\"you@example.com\"\n            required\n          ></aihio-input>\n        </aihio-field>\n\n        <aihio-field data-aihio-intent=\"form-field\">\n          <label slot=\"label\">Password</label>\n          <aihio-input\n            data-aihio-intent=\"text-entry\"\n            type=\"password\"\n            name=\"password\"\n            autocomplete=\"current-password\"\n            required\n          ></aihio-input>\n        </aihio-field>\n      </aihio-stack>\n    </aihio-card-content>\n    <aihio-card-footer>\n      <aihio-cluster data-aihio-intent=\"layout\" grow justify=\"end\">\n        <aihio-button data-aihio-intent=\"secondary-action\" variant=\"outline\">Forgot password</aihio-button>\n        <aihio-button data-aihio-intent=\"primary-action\" type=\"submit\">Sign in</aihio-button>\n      </aihio-cluster>\n    </aihio-card-footer>\n  </form>\n</aihio-card>",
      "variations": [
        {
          "id": "passwordless",
          "name": "Passwordless",
          "description": "Email-only sign-in flow that requests a magic link.",
          "markup": "<aihio-card data-aihio-intent=\"surface\">\n  <form>\n    <aihio-card-header>\n      <aihio-card-title>Email me a sign-in link</aihio-card-title>\n      <aihio-card-description>We will send a one-time login link to your inbox.</aihio-card-description>\n    </aihio-card-header>\n    <aihio-card-content>\n      <aihio-field data-aihio-intent=\"form-field\">\n        <label slot=\"label\">Email</label>\n        <aihio-input\n          data-aihio-intent=\"text-entry\"\n          type=\"email\"\n          name=\"email\"\n          autocomplete=\"email\"\n          placeholder=\"you@example.com\"\n          required\n        ></aihio-input>\n        <span slot=\"description\">The link expires after 10 minutes.</span>\n      </aihio-field>\n    </aihio-card-content>\n    <aihio-card-footer>\n      <aihio-cluster data-aihio-intent=\"layout\" grow justify=\"end\">\n        <aihio-button data-aihio-intent=\"secondary-action\" variant=\"outline\">Use password instead</aihio-button>\n        <aihio-button data-aihio-intent=\"primary-action\" type=\"submit\">Send magic link</aihio-button>\n      </aihio-cluster>\n    </aihio-card-footer>\n  </form>\n</aihio-card>"
        },
        {
          "id": "signup",
          "name": "Sign Up",
          "description": "Account-creation variant with name, email, and password confirmation.",
          "markup": "<aihio-card data-aihio-intent=\"surface\">\n  <form>\n    <aihio-card-header>\n      <aihio-card-title>Create your account</aihio-card-title>\n      <aihio-card-description>Set up a new workspace login for your team.</aihio-card-description>\n    </aihio-card-header>\n    <aihio-card-content>\n      <aihio-stack data-aihio-intent=\"layout\">\n        <aihio-field data-aihio-intent=\"form-field\">\n          <label slot=\"label\">Full name</label>\n          <aihio-input\n            data-aihio-intent=\"text-entry\"\n            name=\"name\"\n            autocomplete=\"name\"\n            placeholder=\"Jane Doe\"\n            required\n          ></aihio-input>\n        </aihio-field>\n\n        <aihio-field data-aihio-intent=\"form-field\">\n          <label slot=\"label\">Email</label>\n          <aihio-input\n            data-aihio-intent=\"text-entry\"\n            type=\"email\"\n            name=\"email\"\n            autocomplete=\"email\"\n            placeholder=\"jane@company.com\"\n            required\n          ></aihio-input>\n        </aihio-field>\n\n        <aihio-field data-aihio-intent=\"form-field\">\n          <label slot=\"label\">Password</label>\n          <aihio-input\n            data-aihio-intent=\"text-entry\"\n            type=\"password\"\n            name=\"password\"\n            autocomplete=\"new-password\"\n            required\n          ></aihio-input>\n          <span slot=\"description\">At least 12 characters, including one symbol.</span>\n        </aihio-field>\n\n        <aihio-field data-aihio-intent=\"form-field\">\n          <label slot=\"label\">Confirm password</label>\n          <aihio-input\n            data-aihio-intent=\"text-entry\"\n            type=\"password\"\n            name=\"password_confirmation\"\n            autocomplete=\"new-password\"\n            required\n          ></aihio-input>\n        </aihio-field>\n      </aihio-stack>\n    </aihio-card-content>\n    <aihio-card-footer>\n      <aihio-cluster data-aihio-intent=\"layout\" grow justify=\"end\">\n        <aihio-button data-aihio-intent=\"secondary-action\" variant=\"outline\">Already have an account?</aihio-button>\n        <aihio-button data-aihio-intent=\"primary-action\" type=\"submit\">Create account</aihio-button>\n      </aihio-cluster>\n    </aihio-card-footer>\n  </form>\n</aihio-card>"
        }
      ]
    },
    {
      "id": "data-card-grid",
      "name": "Data Card Grid",
      "intents": [
        "layout",
        "surface",
        "status",
        "identity",
        "secondary-action"
      ],
      "description": "A dashboard-style grid of compact summary cards with status, ownership, and a follow-up action. aihio-grid drops columns as it narrows, and the cards are a list.",
      "requiredComponents": [
        "aihio-grid",
        "aihio-card",
        "aihio-badge",
        "aihio-avatar",
        "aihio-button",
        "aihio-stack",
        "aihio-cluster"
      ],
      "markup": "<section>\n  <aihio-stack data-aihio-intent=\"layout\" gap=\"lg\">\n    <header>\n      <h2>Active projects</h2>\n      <p>Track ownership, launch stage, and the next review for each project.</p>\n    </header>\n\n    <aihio-grid data-aihio-intent=\"layout\" columns=\"3\" role=\"list\">\n      <aihio-card data-aihio-intent=\"surface\" role=\"listitem\">\n        <aihio-card-header>\n          <aihio-card-title>Customer portal</aihio-card-title>\n          <aihio-card-description>Next review in 3 days.</aihio-card-description>\n        </aihio-card-header>\n        <aihio-card-content>\n          <aihio-stack data-aihio-intent=\"layout\" gap=\"sm\" align=\"start\">\n            <aihio-badge data-aihio-intent=\"status\">Ready</aihio-badge>\n            <aihio-cluster data-aihio-intent=\"layout\">\n              <aihio-avatar data-aihio-intent=\"identity\" alt=\"Jane Doe\" size=\"sm\"></aihio-avatar>\n              <span>Jane Doe</span>\n            </aihio-cluster>\n          </aihio-stack>\n        </aihio-card-content>\n        <aihio-card-footer>\n          <aihio-button data-aihio-intent=\"secondary-action\" variant=\"outline\">Open</aihio-button>\n        </aihio-card-footer>\n      </aihio-card>\n\n      <aihio-card data-aihio-intent=\"surface\" role=\"listitem\">\n        <aihio-card-header>\n          <aihio-card-title>Billing sync</aihio-card-title>\n          <aihio-card-description>Audit notes pending from finance.</aihio-card-description>\n        </aihio-card-header>\n        <aihio-card-content>\n          <aihio-stack data-aihio-intent=\"layout\" gap=\"sm\" align=\"start\">\n            <aihio-badge data-aihio-intent=\"status\" variant=\"warning\">Needs review</aihio-badge>\n            <aihio-cluster data-aihio-intent=\"layout\">\n              <aihio-avatar data-aihio-intent=\"identity\" alt=\"Raine Luntta\" size=\"sm\"></aihio-avatar>\n              <span>Raine Luntta</span>\n            </aihio-cluster>\n          </aihio-stack>\n        </aihio-card-content>\n        <aihio-card-footer>\n          <aihio-button data-aihio-intent=\"secondary-action\" variant=\"outline\">Review</aihio-button>\n        </aihio-card-footer>\n      </aihio-card>\n\n      <aihio-card data-aihio-intent=\"surface\" role=\"listitem\">\n        <aihio-card-header>\n          <aihio-card-title>Ops migration</aihio-card-title>\n          <aihio-card-description>Rollout staged for next Tuesday.</aihio-card-description>\n        </aihio-card-header>\n        <aihio-card-content>\n          <aihio-stack data-aihio-intent=\"layout\" gap=\"sm\" align=\"start\">\n            <aihio-badge data-aihio-intent=\"status\" variant=\"secondary\">Scheduled</aihio-badge>\n            <aihio-cluster data-aihio-intent=\"layout\">\n              <aihio-avatar data-aihio-intent=\"identity\" alt=\"Ada King\" size=\"sm\"></aihio-avatar>\n              <span>Ada King</span>\n            </aihio-cluster>\n          </aihio-stack>\n        </aihio-card-content>\n        <aihio-card-footer>\n          <aihio-button data-aihio-intent=\"secondary-action\" variant=\"outline\">View plan</aihio-button>\n        </aihio-card-footer>\n      </aihio-card>\n    </aihio-grid>\n  </aihio-stack>\n</section>",
      "variations": []
    },
    {
      "id": "data-table",
      "name": "Data Table",
      "intents": [
        "tabular-data",
        "status",
        "menu",
        "primary-action",
        "navigation",
        "form-field",
        "layout"
      ],
      "description": "A list of records to compare and sort: a heading with the primary action, a search form, a table whose columns sort, whose row headers link to each record, and whose rows carry a status and a menu of actions, and aihio-pagination by link. The table is named by the section's heading, and on a narrow screen it scrolls inside its own box.",
      "requiredComponents": [
        "aihio-table",
        "aihio-button",
        "aihio-field",
        "aihio-input",
        "aihio-stack",
        "aihio-cluster"
      ],
      "markup": "<section>\n  <aihio-stack data-aihio-intent=\"layout\" gap=\"md\">\n    <aihio-cluster data-aihio-intent=\"layout\" justify=\"between\">\n      <h2 id=\"invoices-heading\">Invoices</h2>\n      <aihio-button data-aihio-intent=\"primary-action\"><a href=\"/invoices/new\">New invoice</a></aihio-button>\n    </aihio-cluster>\n\n    <form role=\"search\" action=\"/invoices\" method=\"get\">\n      <aihio-field data-aihio-intent=\"form-field\">\n        <label slot=\"label\">Search invoices</label>\n        <aihio-input type=\"search\" name=\"q\" placeholder=\"Customer or invoice number\"></aihio-input>\n      </aihio-field>\n    </form>\n\n    <aihio-table data-aihio-intent=\"tabular-data\">\n      <table aria-labelledby=\"invoices-heading\">\n        <thead>\n          <tr>\n            <th scope=\"col\" data-sortable=\"invoice\">Invoice</th>\n            <th scope=\"col\" data-sortable=\"customer\">Customer</th>\n            <th scope=\"col\" data-sortable=\"status\">Status</th>\n            <th scope=\"col\" data-sortable=\"issued\" aria-sort=\"descending\">Issued</th>\n            <th scope=\"col\" data-sortable=\"amount\" data-numeric>Amount</th>\n            <th scope=\"col\">Actions</th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <th scope=\"row\"><a href=\"/invoices/1046\">INV-1046</a></th>\n            <td>Wide World Importers</td>\n            <td data-sort-value=\"3\"><aihio-badge data-aihio-intent=\"status\" variant=\"secondary\">Draft</aihio-badge></td>\n            <td><time datetime=\"2026-10-02\">2 Oct 2026</time></td>\n            <td data-numeric>€3,200.00</td>\n            <td>\n              <aihio-dropdown data-aihio-intent=\"menu\" align=\"end\">\n                <aihio-button slot=\"trigger\" variant=\"ghost\" size=\"icon\" aria-label=\"Actions for INV-1046\">&#x22ef;</aihio-button>\n                <aihio-dropdown-item>Edit draft</aihio-dropdown-item>\n                <aihio-dropdown-item>Download PDF</aihio-dropdown-item>\n              </aihio-dropdown>\n            </td>\n          </tr>\n          <tr>\n            <th scope=\"row\"><a href=\"/invoices/1045\">INV-1045</a></th>\n            <td>Adventure Works</td>\n            <td data-sort-value=\"2\"><aihio-badge data-aihio-intent=\"status\" variant=\"warning\">Due</aihio-badge></td>\n            <td><time datetime=\"2026-09-30\">30 Sep 2026</time></td>\n            <td data-numeric>€640.00</td>\n            <td>\n              <aihio-dropdown data-aihio-intent=\"menu\" align=\"end\">\n                <aihio-button slot=\"trigger\" variant=\"ghost\" size=\"icon\" aria-label=\"Actions for INV-1045\">&#x22ef;</aihio-button>\n                <aihio-dropdown-item>Send reminder</aihio-dropdown-item>\n                <aihio-dropdown-item>Download PDF</aihio-dropdown-item>\n              </aihio-dropdown>\n            </td>\n          </tr>\n          <tr>\n            <th scope=\"row\"><a href=\"/invoices/1044\">INV-1044</a></th>\n            <td>Contoso</td>\n            <td data-sort-value=\"1\"><aihio-badge data-aihio-intent=\"status\" variant=\"destructive\">Overdue</aihio-badge></td>\n            <td><time datetime=\"2026-09-12\">12 Sep 2026</time></td>\n            <td data-numeric>€12,400.00</td>\n            <td>\n              <aihio-dropdown data-aihio-intent=\"menu\" align=\"end\">\n                <aihio-button slot=\"trigger\" variant=\"ghost\" size=\"icon\" aria-label=\"Actions for INV-1044\">&#x22ef;</aihio-button>\n                <aihio-dropdown-item>Send reminder</aihio-dropdown-item>\n                <aihio-dropdown-item>Download PDF</aihio-dropdown-item>\n              </aihio-dropdown>\n            </td>\n          </tr>\n          <tr>\n            <th scope=\"row\"><a href=\"/invoices/1043\">INV-1043</a></th>\n            <td>Fabrikam</td>\n            <td data-sort-value=\"4\"><aihio-badge data-aihio-intent=\"status\" variant=\"success\">Paid</aihio-badge></td>\n            <td><time datetime=\"2026-09-04\">4 Sep 2026</time></td>\n            <td data-numeric>€980.50</td>\n            <td>\n              <aihio-dropdown data-aihio-intent=\"menu\" align=\"end\">\n                <aihio-button slot=\"trigger\" variant=\"ghost\" size=\"icon\" aria-label=\"Actions for INV-1043\">&#x22ef;</aihio-button>\n                <aihio-dropdown-item>Download PDF</aihio-dropdown-item>\n                <aihio-dropdown-item>Issue refund</aihio-dropdown-item>\n              </aihio-dropdown>\n            </td>\n          </tr>\n          <tr>\n            <th scope=\"row\"><a href=\"/invoices/1042\">INV-1042</a></th>\n            <td>Northwind Traders</td>\n            <td data-sort-value=\"4\"><aihio-badge data-aihio-intent=\"status\" variant=\"success\">Paid</aihio-badge></td>\n            <td><time datetime=\"2026-08-29\">29 Aug 2026</time></td>\n            <td data-numeric>€1,250.00</td>\n            <td>\n              <aihio-dropdown data-aihio-intent=\"menu\" align=\"end\">\n                <aihio-button slot=\"trigger\" variant=\"ghost\" size=\"icon\" aria-label=\"Actions for INV-1042\">&#x22ef;</aihio-button>\n                <aihio-dropdown-item>Download PDF</aihio-dropdown-item>\n                <aihio-dropdown-item>Issue refund</aihio-dropdown-item>\n              </aihio-dropdown>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    </aihio-table>\n\n    <aihio-cluster data-aihio-intent=\"layout\" justify=\"between\">\n      <p>Showing 1–5 of 42 invoices</p>\n      <aihio-pagination data-aihio-intent=\"navigation\" page=\"1\" pages=\"9\" href=\"/invoices?page={page}\" aria-label=\"Invoice pages\"></aihio-pagination>\n    </aihio-cluster>\n  </aihio-stack>\n</section>",
      "variations": [
        {
          "id": "no-results",
          "name": "No Results",
          "description": "A search that matched nothing keeps the column headers, says so in a row spanning them, and links back to every record.",
          "markup": "<section>\n  <aihio-stack data-aihio-intent=\"layout\" gap=\"md\">\n    <aihio-cluster data-aihio-intent=\"layout\" justify=\"between\">\n      <h2 id=\"invoice-search-heading\">Invoices</h2>\n      <aihio-button data-aihio-intent=\"primary-action\"><a href=\"/invoices/new\">New invoice</a></aihio-button>\n    </aihio-cluster>\n\n    <form role=\"search\" action=\"/invoices\" method=\"get\">\n      <aihio-field data-aihio-intent=\"form-field\">\n        <label slot=\"label\">Search invoices</label>\n        <aihio-input type=\"search\" name=\"q\" value=\"acme\" placeholder=\"Customer or invoice number\"></aihio-input>\n      </aihio-field>\n    </form>\n\n    <aihio-table data-aihio-intent=\"tabular-data\">\n      <table aria-labelledby=\"invoice-search-heading\">\n        <thead>\n          <tr>\n            <th scope=\"col\">Invoice</th>\n            <th scope=\"col\">Customer</th>\n            <th scope=\"col\">Status</th>\n            <th scope=\"col\">Issued</th>\n            <th scope=\"col\" data-numeric>Amount</th>\n            <th scope=\"col\">Actions</th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr>\n            <td colspan=\"6\">\n              <aihio-stack data-aihio-intent=\"layout\" gap=\"sm\" align=\"start\">\n                <p>No invoices match “acme”. Check the spelling, or search by invoice number.</p>\n                <aihio-button data-aihio-intent=\"navigation\" variant=\"outline\" size=\"sm\"><a href=\"/invoices\">Show all invoices</a></aihio-button>\n              </aihio-stack>\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    </aihio-table>\n  </aihio-stack>\n</section>"
        }
      ]
    },
    {
      "id": "destructive-confirmation",
      "name": "Destructive Confirmation",
      "intents": [
        "overlay",
        "modal",
        "destructive-action",
        "secondary-action",
        "dismissible"
      ],
      "description": "A modal confirmation flow for irreversible actions with cancel-first ordering. The trigger opens the dialog and Cancel closes it through Invoker Commands, and the confirm button submits the dialog's form, so the flow works without script.",
      "requiredComponents": [
        "aihio-dialog",
        "aihio-button"
      ],
      "markup": "<aihio-button\n  data-aihio-intent=\"destructive-action\"\n  variant=\"destructive\"\n  commandfor=\"delete-project\"\n  command=\"--open\"\n>Delete project</aihio-button>\n\n<aihio-dialog id=\"delete-project\" data-aihio-intent=\"overlay modal dismissible\">\n  <aihio-dialog-header>\n    <aihio-dialog-title>Delete project?</aihio-dialog-title>\n    <aihio-dialog-description>\n      This permanently removes project access, deploy history, and notification rules.\n    </aihio-dialog-description>\n  </aihio-dialog-header>\n  <form method=\"post\">\n    <aihio-dialog-footer>\n      <aihio-button\n        data-aihio-intent=\"secondary-action\"\n        variant=\"outline\"\n        commandfor=\"delete-project\"\n        command=\"--close\"\n      >Cancel</aihio-button>\n      <aihio-button data-aihio-intent=\"destructive-action\" variant=\"destructive\" type=\"submit\">Delete project</aihio-button>\n    </aihio-dialog-footer>\n  </form>\n</aihio-dialog>",
      "variations": []
    },
    {
      "id": "empty-state",
      "name": "Empty State",
      "intents": [
        "surface",
        "informational",
        "primary-action",
        "secondary-action"
      ],
      "description": "A blank-slate surface that explains what is missing and points the user toward the next action.",
      "requiredComponents": [
        "aihio-card",
        "aihio-button"
      ],
      "markup": "<aihio-card data-aihio-intent=\"surface\">\n  <aihio-card-header>\n    <aihio-card-title>No projects yet</aihio-card-title>\n    <aihio-card-description>Create your first project to start collecting feedback.</aihio-card-description>\n  </aihio-card-header>\n  <aihio-card-content>\n    <p>Projects organize deployments, environments, and release notes for your team.</p>\n  </aihio-card-content>\n  <aihio-card-footer>\n    <aihio-button data-aihio-intent=\"secondary-action\" variant=\"outline\">Import from CSV</aihio-button>\n    <aihio-button data-aihio-intent=\"primary-action\">Create project</aihio-button>\n  </aihio-card-footer>\n</aihio-card>",
      "variations": [
        {
          "id": "search-results",
          "name": "Search Results",
          "description": "No-results variant that suggests clearing filters before creating a new item.",
          "markup": "<aihio-card data-aihio-intent=\"surface\">\n  <aihio-card-header>\n    <aihio-card-title>No results for \"ops\"</aihio-card-title>\n    <aihio-card-description>Try a broader term or remove one of your filters.</aihio-card-description>\n  </aihio-card-header>\n  <aihio-card-content>\n    <p>Your current filters are hiding every matching workspace and archived project.</p>\n  </aihio-card-content>\n  <aihio-card-footer>\n    <aihio-button data-aihio-intent=\"secondary-action\" variant=\"outline\">Clear filters</aihio-button>\n    <aihio-button data-aihio-intent=\"primary-action\">Create new project</aihio-button>\n  </aihio-card-footer>\n</aihio-card>"
        }
      ]
    },
    {
      "id": "inline-form-validation",
      "name": "Inline Form With Validation",
      "intents": [
        "layout",
        "form-field",
        "text-entry",
        "alert",
        "primary-action",
        "secondary-action"
      ],
      "description": "A compact form that keeps the invalid field, its message, and the corrective action in one place.",
      "requiredComponents": [
        "aihio-stack",
        "aihio-field",
        "aihio-input",
        "aihio-alert",
        "aihio-cluster",
        "aihio-button"
      ],
      "markup": "<form data-aihio-intent=\"form-field\">\n  <aihio-stack data-aihio-intent=\"layout\">\n    <aihio-field data-aihio-intent=\"form-field\">\n      <label slot=\"label\">Password</label>\n      <aihio-input\n        data-aihio-intent=\"text-entry\"\n        type=\"password\"\n        name=\"password\"\n        autocomplete=\"new-password\"\n        required\n      ></aihio-input>\n      <span slot=\"description\">At least 12 characters, including one symbol.</span>\n      <span slot=\"error\">Password must include at least 12 characters and one symbol.</span>\n    </aihio-field>\n\n    <aihio-alert data-aihio-intent=\"alert\" variant=\"destructive\">\n      <div slot=\"title\">Password is too weak</div>\n      <div slot=\"description\">Update the field above, then resubmit the form.</div>\n    </aihio-alert>\n\n    <aihio-cluster data-aihio-intent=\"layout\" grow justify=\"end\">\n      <aihio-button data-aihio-intent=\"secondary-action\" variant=\"outline\" type=\"reset\">Cancel</aihio-button>\n      <aihio-button data-aihio-intent=\"primary-action\" type=\"submit\">Save password</aihio-button>\n    </aihio-cluster>\n  </aihio-stack>\n</form>",
      "variations": [
        {
          "id": "success",
          "name": "Success",
          "description": "Resolved state after the invalid field has been corrected.",
          "markup": "<form data-aihio-intent=\"form-field\">\n  <aihio-stack data-aihio-intent=\"layout\">\n    <aihio-field data-aihio-intent=\"form-field\">\n      <label slot=\"label\">Password</label>\n      <aihio-input\n        data-aihio-intent=\"text-entry\"\n        type=\"password\"\n        name=\"password\"\n        autocomplete=\"new-password\"\n        value=\"correct horse battery staple\"\n        required\n      ></aihio-input>\n      <span slot=\"description\">Password strength looks good.</span>\n    </aihio-field>\n\n    <aihio-alert data-aihio-intent=\"informational status\" variant=\"success\">\n      <div slot=\"title\">Password accepted</div>\n      <div slot=\"description\">You can submit the form without additional changes.</div>\n    </aihio-alert>\n\n    <aihio-cluster data-aihio-intent=\"layout\" grow justify=\"end\">\n      <aihio-button data-aihio-intent=\"secondary-action\" variant=\"outline\" type=\"reset\">Cancel</aihio-button>\n      <aihio-button data-aihio-intent=\"primary-action\" type=\"submit\">Save password</aihio-button>\n    </aihio-cluster>\n  </aihio-stack>\n</form>"
        }
      ]
    },
    {
      "id": "settings-section",
      "name": "Settings Section",
      "intents": [
        "surface",
        "form-field",
        "text-entry",
        "toggle-state",
        "status",
        "primary-action",
        "secondary-action",
        "layout"
      ],
      "description": "A settings panel in a card: a form of labelled fields — text, a native select for a short list of choices, and a switch for an on/off setting — with Reset and Save that reset and submit it.",
      "requiredComponents": [
        "aihio-card",
        "aihio-field",
        "aihio-input",
        "aihio-switch",
        "aihio-button",
        "aihio-badge",
        "aihio-stack",
        "aihio-cluster"
      ],
      "markup": "<aihio-card data-aihio-intent=\"surface\">\n  <form>\n    <aihio-card-header>\n      <aihio-cluster data-aihio-intent=\"layout\" justify=\"between\">\n        <aihio-card-title>Workspace settings</aihio-card-title>\n        <aihio-badge data-aihio-intent=\"status\" variant=\"secondary\">Live</aihio-badge>\n      </aihio-cluster>\n      <aihio-card-description>Update the defaults your team sees on first login.</aihio-card-description>\n    </aihio-card-header>\n    <aihio-card-content>\n      <aihio-stack data-aihio-intent=\"layout\" gap=\"lg\">\n        <aihio-field data-aihio-intent=\"form-field\">\n          <label slot=\"label\">Workspace name</label>\n          <aihio-input\n            data-aihio-intent=\"text-entry\"\n            name=\"workspace-name\"\n            value=\"Northwind\"\n            required\n          ></aihio-input>\n        </aihio-field>\n\n        <aihio-field data-aihio-intent=\"form-field\">\n          <label slot=\"label\">Default landing page</label>\n          <select name=\"landing-page\">\n            <option value=\"overview\">Overview</option>\n            <option value=\"projects\" selected>Projects</option>\n            <option value=\"settings\">Settings</option>\n          </select>\n          <span slot=\"description\">Where members arrive after they sign in.</span>\n        </aihio-field>\n\n        <aihio-field data-aihio-intent=\"form-field\">\n          <label slot=\"label\">Email notifications</label>\n          <aihio-switch data-aihio-intent=\"toggle-state\" name=\"email-notifications\" checked></aihio-switch>\n          <span slot=\"description\">A digest of workspace activity, sent weekly.</span>\n        </aihio-field>\n      </aihio-stack>\n    </aihio-card-content>\n    <aihio-card-footer>\n      <aihio-cluster data-aihio-intent=\"layout\" grow justify=\"end\">\n        <aihio-button data-aihio-intent=\"secondary-action\" variant=\"outline\" type=\"reset\">Reset</aihio-button>\n        <aihio-button data-aihio-intent=\"primary-action\" type=\"submit\">Save changes</aihio-button>\n      </aihio-cluster>\n    </aihio-card-footer>\n  </form>\n</aihio-card>",
      "variations": []
    },
    {
      "id": "tabbed-settings",
      "name": "Tabbed Settings",
      "intents": [
        "tabs",
        "navigation",
        "layout",
        "surface",
        "form-field",
        "text-entry",
        "toggle-state",
        "primary-action"
      ],
      "description": "A settings view split across tabs so related configuration stays grouped without overwhelming the page. Each panel is its own form with its own Save.",
      "requiredComponents": [
        "aihio-tabs",
        "aihio-card",
        "aihio-field",
        "aihio-input",
        "aihio-switch",
        "aihio-button",
        "aihio-stack",
        "aihio-cluster"
      ],
      "markup": "<aihio-tabs data-aihio-intent=\"tabs navigation layout\" value=\"profile\">\n  <aihio-tab-list>\n    <aihio-tab value=\"profile\">Profile</aihio-tab>\n    <aihio-tab value=\"notifications\">Notifications</aihio-tab>\n    <aihio-tab value=\"security\">Security</aihio-tab>\n  </aihio-tab-list>\n\n  <aihio-tab-panel value=\"profile\">\n    <aihio-card data-aihio-intent=\"surface\">\n      <form>\n        <aihio-card-header>\n          <aihio-card-title>Profile</aihio-card-title>\n          <aihio-card-description>Update how your name and email appear to the team.</aihio-card-description>\n        </aihio-card-header>\n        <aihio-card-content>\n          <aihio-stack data-aihio-intent=\"layout\">\n            <aihio-field data-aihio-intent=\"form-field\">\n              <label slot=\"label\">Full name</label>\n              <aihio-input data-aihio-intent=\"text-entry\" name=\"name\" autocomplete=\"name\" value=\"Jane Doe\" required></aihio-input>\n            </aihio-field>\n            <aihio-field data-aihio-intent=\"form-field\">\n              <label slot=\"label\">Email</label>\n              <aihio-input data-aihio-intent=\"text-entry\" type=\"email\" name=\"email\" autocomplete=\"email\" value=\"jane@company.com\" required></aihio-input>\n            </aihio-field>\n          </aihio-stack>\n        </aihio-card-content>\n        <aihio-card-footer>\n          <aihio-cluster data-aihio-intent=\"layout\" grow justify=\"end\">\n            <aihio-button data-aihio-intent=\"primary-action\" type=\"submit\">Save profile</aihio-button>\n          </aihio-cluster>\n        </aihio-card-footer>\n      </form>\n    </aihio-card>\n  </aihio-tab-panel>\n\n  <aihio-tab-panel value=\"notifications\">\n    <aihio-card data-aihio-intent=\"surface\">\n      <form>\n        <aihio-card-header>\n          <aihio-card-title>Notifications</aihio-card-title>\n          <aihio-card-description>Choose which updates should interrupt your day.</aihio-card-description>\n        </aihio-card-header>\n        <aihio-card-content>\n          <aihio-stack data-aihio-intent=\"layout\" gap=\"lg\">\n            <aihio-field data-aihio-intent=\"form-field\">\n              <label slot=\"label\">Incident digests</label>\n              <aihio-switch data-aihio-intent=\"toggle-state\" name=\"incident-digests\" checked></aihio-switch>\n              <span slot=\"description\">A summary of open incidents every morning.</span>\n            </aihio-field>\n            <aihio-field data-aihio-intent=\"form-field\">\n              <label slot=\"label\">Weekly summary</label>\n              <aihio-switch data-aihio-intent=\"toggle-state\" name=\"weekly-summary\"></aihio-switch>\n              <span slot=\"description\">Activity across every project, sent on Mondays.</span>\n            </aihio-field>\n          </aihio-stack>\n        </aihio-card-content>\n        <aihio-card-footer>\n          <aihio-cluster data-aihio-intent=\"layout\" grow justify=\"end\">\n            <aihio-button data-aihio-intent=\"primary-action\" type=\"submit\">Save notifications</aihio-button>\n          </aihio-cluster>\n        </aihio-card-footer>\n      </form>\n    </aihio-card>\n  </aihio-tab-panel>\n\n  <aihio-tab-panel value=\"security\">\n    <aihio-card data-aihio-intent=\"surface\">\n      <form>\n        <aihio-card-header>\n          <aihio-card-title>Security</aihio-card-title>\n          <aihio-card-description>Rotate credentials and confirm recovery options.</aihio-card-description>\n        </aihio-card-header>\n        <aihio-card-content>\n          <aihio-field data-aihio-intent=\"form-field\">\n            <label slot=\"label\">Recovery email</label>\n            <aihio-input data-aihio-intent=\"text-entry\" type=\"email\" name=\"recovery-email\" autocomplete=\"email\" value=\"security@company.com\"></aihio-input>\n            <span slot=\"description\">Used only if you lose access to your account.</span>\n          </aihio-field>\n        </aihio-card-content>\n        <aihio-card-footer>\n          <aihio-cluster data-aihio-intent=\"layout\" grow justify=\"end\">\n            <aihio-button data-aihio-intent=\"primary-action\" type=\"submit\">Save security settings</aihio-button>\n          </aihio-cluster>\n        </aihio-card-footer>\n      </form>\n    </aihio-card>\n  </aihio-tab-panel>\n</aihio-tabs>",
      "variations": []
    },
    {
      "id": "toast-alert-stack",
      "name": "Toast-Style Alert Stack",
      "intents": [
        "alert",
        "informational",
        "status",
        "layout"
      ],
      "description": "A lightweight stacked-notification pattern for transient system updates.",
      "requiredComponents": [
        "aihio-alert",
        "aihio-stack"
      ],
      "markup": "<aihio-stack aria-live=\"polite\" data-aihio-intent=\"layout\" gap=\"sm\">\n  <aihio-alert data-aihio-intent=\"alert informational\">\n    <div slot=\"title\">Deployment finished</div>\n    <div slot=\"description\">Customer portal v18 is now live in production.</div>\n  </aihio-alert>\n\n  <aihio-alert data-aihio-intent=\"alert status\">\n    <div slot=\"title\">Usage report ready</div>\n    <div slot=\"description\">A new CSV export is available in Billing.</div>\n  </aihio-alert>\n\n  <aihio-alert data-aihio-intent=\"alert\" variant=\"destructive\">\n    <div slot=\"title\">Webhook delivery failed</div>\n    <div slot=\"description\">Three retries have failed for the Slack incident channel.</div>\n  </aihio-alert>\n</aihio-stack>",
      "variations": []
    }
  ]
}