---
version: alpha
name: "Loom Base"
description: "The Loom Base design system (@loom/base), exported by Loom from its records: 2312 tokens, 69 components, 11 patterns."
colors:
  "--base-color-background-canvas": "#f3eee3"
  "--base-color-background-surface": "#fbf8f1"
  "--base-color-background-sunken": "#ebe4d6"
  "--base-color-background-subtle": "#e2d9c8"
  "--base-color-background-inverse": "#17140f"
  "--base-color-background-selected": "#eceefa"
  "--base-color-background-backdrop": "#13110f7a"
  "--base-color-text-default": "#17140f"
  "--base-color-text-subtle": "#4a443a"
  "--base-color-text-disabled": "#8f8571"
  "--base-color-text-inverse": "#f3eee3"
  "--base-color-text-link": "#1e2a78"
  "--base-color-text-link-hover": "#151d55"
  "--base-color-text-accent": "#b02a1e"
  "--base-color-border-default": "#d1c6b2"
  "--base-color-border-strong": "#716856"
  "--base-color-border-inverse": "#17140f"
  "--base-color-border-accent": "#b02a1e"
  "--base-color-border-rule": "#17140f"
  "--base-color-action-primary-background": "#17140f"
  "--base-color-action-primary-background-hover": "#38332b"
  "--base-color-action-primary-background-pressed": "#4a443a"
  "--base-color-action-primary-text": "#fbf8f1"
  "--base-color-action-secondary-background": "#fbf8f1"
  "--base-color-action-secondary-background-hover": "#ebe4d6"
  "--base-color-action-secondary-background-pressed": "#e2d9c8"
  "--base-color-action-secondary-text": "#17140f"
  "--base-color-action-secondary-border": "#17140f"
  "--base-color-action-quiet-background": "#00000000"
  "--base-color-action-quiet-background-hover": "#ebe4d6"
  "--base-color-action-quiet-background-pressed": "#e2d9c8"
  "--base-color-action-quiet-text": "#17140f"
  "--base-color-action-danger-background": "#a0312a"
  "--base-color-action-danger-background-hover": "#7f1814"
  "--base-color-action-danger-background-pressed": "#600204"
  "--base-color-action-danger-text": "#fefdfb"
  "--base-color-action-disabled-background": "#e2d9c8"
  "--base-color-action-disabled-border": "#d1c6b2"
  "--base-color-control-background": "#fbf8f1"
  "--base-color-control-background-hover": "#f3eee3"
  "--base-color-control-checked": "#1e2a78"
  "--base-color-control-on-checked": "#fbf8f1"
  "--base-color-control-track": "#d1c6b2"
  "--base-color-control-thumb": "#fbf8f1"
  "--base-color-focus-ring": "#1e2a78"
  "--base-color-status-info-background": "#e8f1ff"
  "--base-color-status-info-border": "#5478ba"
  "--base-color-status-info-text": "#152e5e"
  "--base-color-status-success-background": "#d5fcde"
  "--base-color-status-success-border": "#3f8b58"
  "--base-color-status-success-text": "#003b1a"
  "--base-color-status-warning-background": "#ffedd8"
  "--base-color-status-warning-border": "#a46b0c"
  "--base-color-status-warning-text": "#452a00"
  "--base-color-status-danger-background": "#ffebe8"
  "--base-color-status-danger-border": "#a0312a"
  "--base-color-status-danger-text": "#7f1814"
  "--base-color-status-neutral-background": "#e8e6e1"
  "--base-color-status-neutral-border": "#c0bdb8"
  "--base-color-status-neutral-text": "#32302c"
  "--base-color-ai-background": "#f2edff"
  "--base-color-ai-border": "#826bab"
  "--base-color-ai-text": "#4d386f"
  "--base-color-field-indigo-background": "#1e2a78"
  "--base-color-field-madder-background": "#b02a1e"
  "--base-color-field-saffron-background": "#e9a21b"
  "--base-color-field-teal-background": "#0b6b66"
  "--base-color-field-ink-background": "#17140f"
  "--base-color-field-on-dark-text": "#f3eee3"
  "--base-color-field-on-dark-text-subtle": "#e2d9c8"
  "--base-color-field-on-dark-border": "#f3eee3"
  "--base-color-field-on-dark-focus": "#f3eee3"
  "--base-color-field-on-dark-action": "#fbf8f1"
  "--base-color-field-on-dark-action-hover": "#ffffff"
  "--base-color-field-on-dark-action-pressed": "#e2d9c8"
  "--base-color-field-on-dark-action-text": "#17140f"
  "--base-color-field-on-light-text": "#17140f"
  "--base-color-field-on-light-text-subtle": "#26221c"
  "--base-color-field-on-light-border": "#17140f"
  "--base-color-field-on-light-focus": "#17140f"
  "--base-color-field-on-light-action": "#17140f"
  "--base-color-field-on-light-action-hover": "#38332b"
  "--base-color-field-on-light-action-pressed": "#4a443a"
  "--base-color-field-on-light-action-text": "#fbf8f1"
  "--base-color-thread-see": "#6f7fe0"
  "--base-color-thread-shape": "#d4574a"
  "--base-color-thread-govern": "#e9a21b"
  "--base-color-thread-run": "#1d948c"
  "--base-color-thread-slack": "#38332b"
  "--base-color-health-woven-thread": "#1e2a78"
  "--base-color-health-woven-ground": "#dde1f6"
  "--base-color-health-holding-thread": "#0b6b66"
  "--base-color-health-holding-ground": "#c5e6e2"
  "--base-color-health-shape-thread": "#9e6b08"
  "--base-color-health-shape-ground": "#fbe7bd"
  "--base-color-health-loose-thread": "#b02a1e"
  "--base-color-health-loose-ground": "#fbecea"
  "--base-color-health-chip-background": "#fbf8f1"
  "--base-color-health-chip-text": "#17140f"
typography:
  "--base-font-family-sans": { fontFamily: "'Atkinson Hyperlegible Next', 'Noto Sans JP', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic UI', 'Yu Gothic', Meiryo, system-ui, sans-serif" }
  "--base-font-family-mono": { fontFamily: "'Martian Mono', ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace" }
  "--base-font-family-display": { fontFamily: "Archivo, 'Noto Sans JP', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic UI', 'Yu Gothic', Meiryo, system-ui, sans-serif" }
  "--base-font-family-overline": { fontFamily: "'Martian Mono', ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace" }
  "--base-font-size-display": { fontSize: "64px" }
  "--base-font-size-headline-lg": { fontSize: "36px" }
  "--base-font-size-headline-md": { fontSize: "30px" }
  "--base-font-size-headline-sm": { fontSize: "24px" }
  "--base-font-size-title": { fontSize: "20px" }
  "--base-font-size-subtitle": { fontSize: "18px" }
  "--base-font-size-body-lg": { fontSize: "18px" }
  "--base-font-size-body": { fontSize: "16px" }
  "--base-font-size-body-sm": { fontSize: "14px" }
  "--base-font-size-label": { fontSize: "14px" }
  "--base-font-size-caption": { fontSize: "13px" }
  "--base-font-size-overline": { fontSize: "12px" }
  "--base-font-size-page-title": { fontSize: "56px" }
  "--base-font-size-figure": { fontSize: "72px" }
  "--base-font-size-masthead": { fontSize: "72px" }
  "--base-font-size-numeral": { fontSize: "120px" }
  "--base-font-size-band": { fontSize: "96px" }
  "--base-font-weight-regular": { fontWeight: 400 }
  "--base-font-weight-medium": { fontWeight: 500 }
  "--base-font-weight-strong": { fontWeight: 600 }
  "--base-font-weight-display": { fontWeight: 900 }
  "--base-font-weight-numeral": { fontWeight: 900 }
rounded:
  "--base-radius-control": "2px"
  "--base-radius-surface": "2px"
  "--base-radius-pill": "9999px"
  "--base-radius-none": "0px"
spacing:
  "--base-space-inset-xs": "4px"
  "--base-space-inset-sm": "8px"
  "--base-space-inset-md": "16px"
  "--base-space-inset-lg": "24px"
  "--base-space-inset-control-x": "16px"
  "--base-space-stack-xs": "4px"
  "--base-space-stack-sm": "8px"
  "--base-space-stack-md": "16px"
  "--base-space-stack-lg": "24px"
  "--base-space-stack-xl": "40px"
  "--base-space-inline-xs": "4px"
  "--base-space-inline-sm": "8px"
  "--base-space-inline-md": "16px"
  "--base-space-inline-lg": "24px"
  "--base-space-layout-gutter": "16px"
  "--base-space-layout-section": "48px"
  "--base-space-weave": "4px"
components:
  base-ai-status: { backgroundColor: "{colors.--base-color-ai-background}", rounded: "{rounded.--base-radius-surface}", padding: "{spacing.--base-space-inset-md}" }
  base-ai-status-detail: { textColor: "{colors.--base-color-text-default}" }
  base-ai-status-error: { backgroundColor: "{colors.--base-color-status-danger-background}" }
  base-ai-status-icon: { width: "20px", height: "20px", textColor: "{colors.--base-color-ai-text}" }
  base-ai-status-icon-error: { textColor: "{colors.--base-color-status-danger-text}" }
  base-ai-status-icon-partial-failure: { textColor: "{colors.--base-color-status-warning-text}" }
  base-ai-status-partial-failure: { backgroundColor: "{colors.--base-color-status-warning-background}" }
  base-ai-status-step: { textColor: "{colors.--base-color-text-default}" }
  base-ai-status-title: { textColor: "{colors.--base-color-text-default}" }
  base-alert-danger: { backgroundColor: "{colors.--base-color-status-danger-background}" }
  base-alert-dismiss: { rounded: "{rounded.--base-radius-control}", textColor: "{colors.--base-color-text-default}" }
  base-alert-dismiss-hover: { backgroundColor: "{colors.--base-color-background-subtle}" }
  base-alert-icon-danger: { textColor: "{colors.--base-color-status-danger-text}" }
  base-alert-icon-info: { textColor: "{colors.--base-color-status-info-text}" }
  base-alert-icon-success: { textColor: "{colors.--base-color-status-success-text}" }
  base-alert-icon-warning: { textColor: "{colors.--base-color-status-warning-text}" }
  base-alert-info: { backgroundColor: "{colors.--base-color-status-info-background}" }
  base-alert-message: { textColor: "{colors.--base-color-text-default}" }
  base-alert-success: { backgroundColor: "{colors.--base-color-status-success-background}" }
  base-alert-title: { textColor: "{colors.--base-color-text-default}" }
  base-alert-warning: { backgroundColor: "{colors.--base-color-status-warning-background}" }
  base-app-header: { backgroundColor: "{colors.--base-color-background-canvas}" }
  base-app-header-brand: { textColor: "{colors.--base-color-text-default}", rounded: "{rounded.--base-radius-control}" }
  base-app-header-context: { textColor: "{colors.--base-color-text-subtle}" }
  base-app-shell: { backgroundColor: "{colors.--base-color-background-canvas}", textColor: "{colors.--base-color-text-default}" }
  base-avatar: { rounded: "{rounded.--base-radius-pill}", backgroundColor: "{colors.--base-color-status-neutral-background}" }
  base-avatar-initials: { textColor: "{colors.--base-color-status-neutral-text}" }
  base-badge-ai: { backgroundColor: "{colors.--base-color-ai-background}", textColor: "{colors.--base-color-ai-text}" }
  base-badge-danger: { backgroundColor: "{colors.--base-color-status-danger-background}", textColor: "{colors.--base-color-status-danger-text}" }
  base-badge-info: { backgroundColor: "{colors.--base-color-status-info-background}", textColor: "{colors.--base-color-status-info-text}" }
  base-badge-neutral: { backgroundColor: "{colors.--base-color-status-neutral-background}", textColor: "{colors.--base-color-status-neutral-text}" }
  base-badge-success: { backgroundColor: "{colors.--base-color-status-success-background}", textColor: "{colors.--base-color-status-success-text}" }
  base-badge-warning: { backgroundColor: "{colors.--base-color-status-warning-background}", textColor: "{colors.--base-color-status-warning-text}" }
  base-band-scale-bandLabel: { textColor: "{colors.--base-color-text-default}" }
  base-band-scale-heading: { textColor: "{colors.--base-color-text-default}" }
  base-band-scale-item-holding: { textColor: "{colors.--base-color-health-holding-thread}" }
  base-band-scale-item-loose: { textColor: "{colors.--base-color-health-loose-thread}" }
  base-band-scale-item-shape: { textColor: "{colors.--base-color-health-shape-thread}" }
  base-band-scale-item-woven: { textColor: "{colors.--base-color-health-woven-thread}" }
  base-band-scale-marker: { textColor: "{colors.--base-color-text-accent}" }
  base-band-scale-swatch: { height: "40px", backgroundColor: "{colors.--base-color-background-sunken}" }
  base-band-scale-swatch-holding: { backgroundColor: "{colors.--base-color-health-holding-ground}" }
  base-band-scale-swatch-loose: { backgroundColor: "{colors.--base-color-health-loose-ground}" }
  base-band-scale-swatch-shape: { backgroundColor: "{colors.--base-color-health-shape-ground}" }
  base-band-scale-swatch-woven: { backgroundColor: "{colors.--base-color-health-woven-ground}" }
  base-box: { textColor: "{colors.--base-color-text-default}" }
  base-box-canvas: { backgroundColor: "{colors.--base-color-background-canvas}" }
  base-box-sunken: { backgroundColor: "{colors.--base-color-background-sunken}" }
  base-box-surface: { backgroundColor: "{colors.--base-color-background-surface}", rounded: "{rounded.--base-radius-surface}" }
  base-breadcrumbs-current: { textColor: "{colors.--base-color-text-subtle}" }
  base-breadcrumbs-link: { textColor: "{colors.--base-color-text-link}", rounded: "{rounded.--base-radius-control}" }
  base-breadcrumbs-link-hover: { textColor: "{colors.--base-color-text-link-hover}" }
  base-button-danger: { backgroundColor: "{colors.--base-color-action-danger-background}", textColor: "{colors.--base-color-action-danger-text}" }
  base-button-danger-hover: { backgroundColor: "{colors.--base-color-action-danger-background-hover}" }
  base-button-danger-pressed: { backgroundColor: "{colors.--base-color-action-danger-background-pressed}" }
  base-button-disabled: { backgroundColor: "{colors.--base-color-action-disabled-background}", textColor: "{colors.--base-color-text-disabled}" }
  base-button-primary: { backgroundColor: "{colors.--base-color-action-primary-background}", textColor: "{colors.--base-color-action-primary-text}" }
  base-button-primary-hover: { backgroundColor: "{colors.--base-color-action-primary-background-hover}" }
  base-button-primary-pressed: { backgroundColor: "{colors.--base-color-action-primary-background-pressed}" }
  base-button-quiet: { backgroundColor: "{colors.--base-color-action-quiet-background}", textColor: "{colors.--base-color-action-quiet-text}" }
  base-button-quiet-hover: { backgroundColor: "{colors.--base-color-action-quiet-background-hover}" }
  base-button-quiet-pressed: { backgroundColor: "{colors.--base-color-action-quiet-background-pressed}" }
  base-button-secondary: { backgroundColor: "{colors.--base-color-action-secondary-background}", textColor: "{colors.--base-color-action-secondary-text}" }
  base-button-secondary-hover: { backgroundColor: "{colors.--base-color-action-secondary-background-hover}" }
  base-button-secondary-pressed: { backgroundColor: "{colors.--base-color-action-secondary-background-pressed}" }
  base-button-selected: { backgroundColor: "{colors.--base-color-background-selected}", textColor: "{colors.--base-color-text-link}" }
  base-card-body: { textColor: "{colors.--base-color-text-default}" }
  base-card-description: { textColor: "{colors.--base-color-text-subtle}" }
  base-card-outlined: { backgroundColor: "{colors.--base-color-background-surface}" }
  base-card-raised: { backgroundColor: "{colors.--base-color-background-surface}" }
  base-card-sunken: { backgroundColor: "{colors.--base-color-background-sunken}" }
  base-card-title: { textColor: "{colors.--base-color-text-default}" }
  base-checkbox-box: { width: "16px", height: "16px", rounded: "{rounded.--base-radius-control}", backgroundColor: "{colors.--base-color-control-background}" }
  base-checkbox-box-checked: { backgroundColor: "{colors.--base-color-control-checked}" }
  base-checkbox-box-disabled: { backgroundColor: "{colors.--base-color-action-disabled-background}" }
  base-checkbox-group-control: { width: "16px", height: "16px", rounded: "{rounded.--base-radius-control}", backgroundColor: "{colors.--base-color-control-background}" }
  base-checkbox-group-control-disabled: { backgroundColor: "{colors.--base-color-action-disabled-background}" }
  base-checkbox-group-control-selected: { backgroundColor: "{colors.--base-color-control-checked}" }
  base-checkbox-group-error: { textColor: "{colors.--base-color-status-danger-text}" }
  base-checkbox-group-hint: { textColor: "{colors.--base-color-text-subtle}" }
  base-checkbox-group-itemLabel: { textColor: "{colors.--base-color-text-default}" }
  base-checkbox-group-itemLabel-disabled: { textColor: "{colors.--base-color-text-disabled}" }
  base-checkbox-group-label: { textColor: "{colors.--base-color-text-default}" }
  base-checkbox-group-mark: { textColor: "{colors.--base-color-control-on-checked}" }
  base-checkbox-hint: { textColor: "{colors.--base-color-text-subtle}" }
  base-checkbox-label: { textColor: "{colors.--base-color-text-default}" }
  base-checkbox-label-disabled: { textColor: "{colors.--base-color-text-disabled}" }
  base-checkbox-mark: { textColor: "{colors.--base-color-control-on-checked}" }
  base-citation-link: { textColor: "{colors.--base-color-ai-text}", rounded: "{rounded.--base-radius-control}", backgroundColor: "{colors.--base-color-ai-background}" }
  base-cloth-caption: { textColor: "{colors.--base-color-text-default}" }
  base-cloth-cell-holding: { backgroundColor: "{colors.--base-color-health-holding-ground}" }
  base-cloth-cell-loose: { backgroundColor: "{colors.--base-color-health-loose-ground}" }
  base-cloth-cell-shape: { backgroundColor: "{colors.--base-color-health-shape-ground}" }
  base-cloth-cell-table: { backgroundColor: "{colors.--base-color-background-surface}" }
  base-cloth-cell-woven: { backgroundColor: "{colors.--base-color-health-woven-ground}" }
  base-cloth-cellCount: { textColor: "{colors.--base-color-health-chip-text}" }
  base-cloth-cellLabel: { textColor: "{colors.--base-color-health-chip-text}" }
  base-cloth-corner: { textColor: "{colors.--base-color-text-subtle}" }
  base-cloth-family: { textColor: "{colors.--base-color-text-default}" }
  base-cloth-glyph-holding: { textColor: "{colors.--base-color-health-holding-thread}" }
  base-cloth-glyph-loose: { textColor: "{colors.--base-color-health-loose-thread}" }
  base-cloth-glyph-shape: { textColor: "{colors.--base-color-health-shape-thread}" }
  base-cloth-glyph-woven: { textColor: "{colors.--base-color-health-woven-thread}" }
  base-cloth-product: { textColor: "{colors.--base-color-text-default}" }
  base-cloth-tag: { backgroundColor: "{colors.--base-color-health-chip-background}", textColor: "{colors.--base-color-health-chip-text}", rounded: "{rounded.--base-radius-control}" }
  base-cloth-weave-holding: { textColor: "{colors.--base-color-health-holding-thread}" }
  base-cloth-weave-loose: { textColor: "{colors.--base-color-health-loose-thread}" }
  base-cloth-weave-shape: { textColor: "{colors.--base-color-health-shape-thread}" }
  base-cloth-weave-woven: { textColor: "{colors.--base-color-health-woven-thread}" }
  base-colour-block-description-indigo: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-description-madder: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-description-saffron: { textColor: "{colors.--base-color-field-on-light-text}" }
  base-colour-block-description-teal: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-indigo: { backgroundColor: "{colors.--base-color-field-indigo-background}" }
  base-colour-block-label-indigo: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-label-madder: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-label-saffron: { textColor: "{colors.--base-color-field-on-light-text}" }
  base-colour-block-label-teal: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-link-indigo: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-link-madder: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-link-saffron: { textColor: "{colors.--base-color-field-on-light-text}" }
  base-colour-block-link-teal: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-linkIcon-indigo: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-linkIcon-madder: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-linkIcon-saffron: { textColor: "{colors.--base-color-field-on-light-text}" }
  base-colour-block-linkIcon-teal: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-madder: { backgroundColor: "{colors.--base-color-field-madder-background}" }
  base-colour-block-saffron: { backgroundColor: "{colors.--base-color-field-saffron-background}" }
  base-colour-block-teal: { backgroundColor: "{colors.--base-color-field-teal-background}" }
  base-colour-block-value-indigo: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-value-madder: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-colour-block-value-saffron: { textColor: "{colors.--base-color-field-on-light-text}" }
  base-colour-block-value-teal: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-combo-box-button: { textColor: "{colors.--base-color-text-subtle}" }
  base-combo-box-control: { backgroundColor: "{colors.--base-color-control-background}", rounded: "{rounded.--base-radius-control}", textColor: "{colors.--base-color-text-default}" }
  base-combo-box-control-disabled: { backgroundColor: "{colors.--base-color-action-disabled-background}", textColor: "{colors.--base-color-text-disabled}" }
  base-combo-box-control-hover: { backgroundColor: "{colors.--base-color-control-background-hover}" }
  base-combo-box-empty: { textColor: "{colors.--base-color-text-subtle}" }
  base-combo-box-error: { textColor: "{colors.--base-color-status-danger-text}" }
  base-combo-box-hint: { textColor: "{colors.--base-color-text-subtle}" }
  base-combo-box-label: { textColor: "{colors.--base-color-text-default}" }
  base-combo-box-option: { textColor: "{colors.--base-color-text-default}" }
  base-combo-box-option-disabled: { textColor: "{colors.--base-color-text-disabled}" }
  base-combo-box-option-focus-visible: { backgroundColor: "{colors.--base-color-background-subtle}" }
  base-combo-box-option-hover: { backgroundColor: "{colors.--base-color-background-subtle}" }
  base-combo-box-option-selected: { backgroundColor: "{colors.--base-color-background-selected}" }
  base-combo-box-popover: { backgroundColor: "{colors.--base-color-background-surface}", rounded: "{rounded.--base-radius-control}" }
  base-combo-box-required: { textColor: "{colors.--base-color-status-danger-text}" }
  base-confirm-action: { backgroundColor: "{colors.--base-color-background-surface}", rounded: "{rounded.--base-radius-surface}", padding: "{spacing.--base-space-inset-md}" }
  base-confirm-action-description: { textColor: "{colors.--base-color-text-subtle}" }
  base-confirm-action-outcome: { textColor: "{colors.--base-color-text-subtle}" }
  base-confirm-action-step: { textColor: "{colors.--base-color-text-default}" }
  base-confirm-action-title: { textColor: "{colors.--base-color-text-default}" }
  base-dialog: { backgroundColor: "{colors.--base-color-background-surface}", textColor: "{colors.--base-color-text-default}", rounded: "{rounded.--base-radius-surface}", padding: "{spacing.--base-space-inset-lg}" }
  base-dialog-backdrop: { backgroundColor: "{colors.--base-color-background-backdrop}", padding: "{spacing.--base-space-inset-md}" }
  base-dialog-body: { textColor: "{colors.--base-color-text-default}" }
  base-dialog-close: { rounded: "{rounded.--base-radius-control}", textColor: "{colors.--base-color-text-subtle}" }
  base-dialog-close-hover: { backgroundColor: "{colors.--base-color-background-subtle}" }
  base-dialog-description: { textColor: "{colors.--base-color-text-subtle}" }
  base-dialog-title: { textColor: "{colors.--base-color-text-default}" }
  base-disclosure-chevron: { width: "20px", height: "20px", textColor: "{colors.--base-color-text-subtle}" }
  base-disclosure-content: { textColor: "{colors.--base-color-text-default}" }
  base-disclosure-trigger: { textColor: "{colors.--base-color-text-default}", rounded: "{rounded.--base-radius-control}" }
  base-disclosure-trigger-hover: { backgroundColor: "{colors.--base-color-background-subtle}" }
  base-drawer: { backgroundColor: "{colors.--base-color-background-surface}", textColor: "{colors.--base-color-text-default}", padding: "{spacing.--base-space-inset-lg}", width: "480px" }
  base-drawer-backdrop: { backgroundColor: "{colors.--base-color-background-backdrop}" }
  base-drawer-body: { textColor: "{colors.--base-color-text-default}" }
  base-drawer-close: { rounded: "{rounded.--base-radius-control}", textColor: "{colors.--base-color-text-subtle}" }
  base-drawer-close-hover: { backgroundColor: "{colors.--base-color-background-subtle}" }
  base-drawer-description: { textColor: "{colors.--base-color-text-subtle}" }
  base-drawer-title: { textColor: "{colors.--base-color-text-default}" }
  base-empty-state-description: { textColor: "{colors.--base-color-text-subtle}" }
  base-empty-state-icon: { width: "24px", height: "24px", textColor: "{colors.--base-color-text-subtle}" }
  base-empty-state-title: { textColor: "{colors.--base-color-text-default}" }
  base-error-summary: { backgroundColor: "{colors.--base-color-background-surface}", rounded: "{rounded.--base-radius-surface}", padding: "{spacing.--base-space-inset-md}" }
  base-error-summary-link: { textColor: "{colors.--base-color-status-danger-text}", rounded: "{rounded.--base-radius-control}" }
  base-error-summary-title: { textColor: "{colors.--base-color-text-default}" }
  base-feedback-controls-thanks: { textColor: "{colors.--base-color-text-subtle}" }
  base-form-field-error: { textColor: "{colors.--base-color-status-danger-text}" }
  base-form-field-hint: { textColor: "{colors.--base-color-text-subtle}" }
  base-form-field-label: { textColor: "{colors.--base-color-text-default}" }
  base-form-field-required: { textColor: "{colors.--base-color-status-danger-text}" }
  base-heading: { textColor: "{colors.--base-color-text-default}" }
  base-health-band-edge-indigo: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-health-band-edge-madder: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-health-band-edge-saffron: { textColor: "{colors.--base-color-field-on-light-text}" }
  base-health-band-edge-teal: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-health-band-eyebrow-indigo: { textColor: "{colors.--base-color-field-on-dark-text-subtle}" }
  base-health-band-eyebrow-madder: { textColor: "{colors.--base-color-field-on-dark-text-subtle}" }
  base-health-band-eyebrow-saffron: { textColor: "{colors.--base-color-field-on-light-text-subtle}" }
  base-health-band-eyebrow-teal: { textColor: "{colors.--base-color-field-on-dark-text-subtle}" }
  base-health-band-headline-indigo: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-health-band-headline-madder: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-health-band-headline-saffron: { textColor: "{colors.--base-color-field-on-light-text}" }
  base-health-band-headline-teal: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-health-band-indigo: { backgroundColor: "{colors.--base-color-field-indigo-background}" }
  base-health-band-lede-indigo: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-health-band-lede-madder: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-health-band-lede-saffron: { textColor: "{colors.--base-color-field-on-light-text}" }
  base-health-band-lede-teal: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-health-band-madder: { backgroundColor: "{colors.--base-color-field-madder-background}" }
  base-health-band-saffron: { backgroundColor: "{colors.--base-color-field-saffron-background}" }
  base-health-band-teal: { backgroundColor: "{colors.--base-color-field-teal-background}" }
  base-icon-glyph-ai: { textColor: "{colors.--base-color-ai-text}" }
  base-icon-glyph-danger: { textColor: "{colors.--base-color-status-danger-text}" }
  base-icon-glyph-info: { textColor: "{colors.--base-color-status-info-text}" }
  base-icon-glyph-subtle: { textColor: "{colors.--base-color-text-subtle}" }
  base-icon-glyph-success: { textColor: "{colors.--base-color-status-success-text}" }
  base-icon-glyph-warning: { textColor: "{colors.--base-color-status-warning-text}" }
  base-link: { textColor: "{colors.--base-color-text-link}", rounded: "{rounded.--base-radius-control}" }
  base-link-hover: { textColor: "{colors.--base-color-text-link-hover}" }
  base-loose-threads-bandLabel: { textColor: "{colors.--base-color-text-subtle}" }
  base-loose-threads-cellLink: { textColor: "{colors.--base-color-text-link}" }
  base-loose-threads-knot-holding: { textColor: "{colors.--base-color-health-holding-thread}" }
  base-loose-threads-knot-loose: { textColor: "{colors.--base-color-health-loose-thread}" }
  base-loose-threads-knot-shape: { textColor: "{colors.--base-color-health-shape-thread}" }
  base-loose-threads-knot-woven: { textColor: "{colors.--base-color-health-woven-thread}" }
  base-loose-threads-link: { textColor: "{colors.--base-color-text-default}" }
  base-loose-threads-summary: { textColor: "{colors.--base-color-text-subtle}" }
  base-loose-threads-title: { textColor: "{colors.--base-color-text-default}" }
  base-loose-threads-where: { textColor: "{colors.--base-color-text-subtle}" }
  base-masthead: { backgroundColor: "{colors.--base-color-background-canvas}" }
  base-masthead-context: { textColor: "{colors.--base-color-text-default}" }
  base-masthead-dateline: { textColor: "{colors.--base-color-text-default}" }
  base-masthead-issue: { textColor: "{colors.--base-color-text-default}" }
  base-masthead-note: { textColor: "{colors.--base-color-text-default}" }
  base-masthead-title: { textColor: "{colors.--base-color-text-default}" }
  base-masthead-titleAccent: { textColor: "{colors.--base-color-text-accent}" }
  base-menu-disabled: { textColor: "{colors.--base-color-text-disabled}" }
  base-menu-hover: { backgroundColor: "{colors.--base-color-action-secondary-background-hover}" }
  base-menu-item: { textColor: "{colors.--base-color-text-default}" }
  base-menu-item-disabled: { textColor: "{colors.--base-color-text-disabled}" }
  base-menu-item-focus-visible: { backgroundColor: "{colors.--base-color-background-subtle}" }
  base-menu-item-hover: { backgroundColor: "{colors.--base-color-background-subtle}" }
  base-menu-item-selected: { backgroundColor: "{colors.--base-color-background-selected}" }
  base-menu-popover: { backgroundColor: "{colors.--base-color-background-surface}", rounded: "{rounded.--base-radius-control}" }
  base-menu-pressed: { backgroundColor: "{colors.--base-color-action-secondary-background-pressed}" }
  base-menu-quiet: { backgroundColor: "{colors.--base-color-action-quiet-background}", textColor: "{colors.--base-color-action-quiet-text}" }
  base-menu-secondary: { backgroundColor: "{colors.--base-color-action-secondary-background}", textColor: "{colors.--base-color-action-secondary-text}" }
  base-numeral-caption: { textColor: "{colors.--base-color-text-default}" }
  base-numeral-label: { textColor: "{colors.--base-color-text-default}" }
  base-numeral-unit: { textColor: "{colors.--base-color-text-accent}" }
  base-numeral-value: { textColor: "{colors.--base-color-text-default}" }
  base-page-header-description: { textColor: "{colors.--base-color-text-subtle}" }
  base-page-header-eyebrow: { textColor: "{colors.--base-color-text-accent}" }
  base-page-header-title: { textColor: "{colors.--base-color-text-default}" }
  base-pagination-gap: { textColor: "{colors.--base-color-text-subtle}" }
  base-pagination-page: { rounded: "{rounded.--base-radius-control}", backgroundColor: "{colors.--base-color-action-quiet-background}", textColor: "{colors.--base-color-text-default}" }
  base-pagination-page-current: { backgroundColor: "{colors.--base-color-background-surface}" }
  base-pagination-page-hover: { backgroundColor: "{colors.--base-color-action-quiet-background-hover}" }
  base-pagination-step: { rounded: "{rounded.--base-radius-control}", backgroundColor: "{colors.--base-color-action-quiet-background}", textColor: "{colors.--base-color-action-quiet-text}" }
  base-pagination-step-disabled: { textColor: "{colors.--base-color-text-disabled}" }
  base-pagination-step-hover: { backgroundColor: "{colors.--base-color-action-quiet-background-hover}" }
  base-popover-body: { textColor: "{colors.--base-color-text-default}" }
  base-popover-popover: { backgroundColor: "{colors.--base-color-background-surface}", rounded: "{rounded.--base-radius-control}", padding: "{spacing.--base-space-inset-md}" }
  base-popover-title: { textColor: "{colors.--base-color-text-default}" }
  base-popover-trigger-ai: { backgroundColor: "{colors.--base-color-ai-background}", textColor: "{colors.--base-color-ai-text}", rounded: "{rounded.--base-radius-pill}" }
  base-popover-trigger-hover: { backgroundColor: "{colors.--base-color-action-secondary-background-hover}" }
  base-popover-trigger-quiet: { backgroundColor: "{colors.--base-color-action-quiet-background}", textColor: "{colors.--base-color-action-quiet-text}" }
  base-popover-trigger-secondary: { backgroundColor: "{colors.--base-color-action-secondary-background}", textColor: "{colors.--base-color-action-secondary-text}" }
  base-progress-bar-label: { textColor: "{colors.--base-color-text-default}" }
  base-progress-bar-track: { height: "6px", backgroundColor: "{colors.--base-color-control-track}", rounded: "{rounded.--base-radius-pill}" }
  base-progress-bar-value: { textColor: "{colors.--base-color-text-subtle}" }
  base-proof-diff-from: { textColor: "{colors.--base-color-text-subtle}" }
  base-proof-diff-to: { textColor: "{colors.--base-color-text-accent}" }
  base-provider: { backgroundColor: "{colors.--base-color-background-canvas}", textColor: "{colors.--base-color-text-default}" }
  base-radio-group-control: { width: "16px", height: "16px", rounded: "{rounded.--base-radius-pill}", backgroundColor: "{colors.--base-color-control-background}", textColor: "{colors.--base-color-control-checked}" }
  base-radio-group-control-disabled: { backgroundColor: "{colors.--base-color-action-disabled-background}" }
  base-radio-group-dot: { backgroundColor: "{colors.--base-color-control-checked}" }
  base-radio-group-error: { textColor: "{colors.--base-color-status-danger-text}" }
  base-radio-group-hint: { textColor: "{colors.--base-color-text-subtle}" }
  base-radio-group-itemLabel: { textColor: "{colors.--base-color-text-default}" }
  base-radio-group-itemLabel-disabled: { textColor: "{colors.--base-color-text-disabled}" }
  base-radio-group-label: { textColor: "{colors.--base-color-text-default}" }
  base-seal: { width: "96px", height: "96px", padding: "{spacing.--base-space-inset-xs}", rounded: "{rounded.--base-radius-control}", backgroundColor: "{colors.--base-color-background-surface}" }
  base-seal-emptyMark: { textColor: "{colors.--base-color-text-subtle}" }
  base-seal-emptyText: { textColor: "{colors.--base-color-text-accent}" }
  base-seal-sealedMark: { textColor: "{colors.--base-color-text-accent}" }
  base-seal-sealedName: { textColor: "{colors.--base-color-text-accent}" }
  base-section-nav-count: { textColor: "{colors.--base-color-text-subtle}" }
  base-section-nav-link: { textColor: "{colors.--base-color-text-default}" }
  base-select-check: { width: "16px", height: "16px", textColor: "{colors.--base-color-text-link}" }
  base-select-chevron: { width: "20px", height: "20px", textColor: "{colors.--base-color-text-subtle}" }
  base-select-error: { textColor: "{colors.--base-color-status-danger-text}" }
  base-select-hint: { textColor: "{colors.--base-color-text-subtle}" }
  base-select-label: { textColor: "{colors.--base-color-text-default}" }
  base-select-option: { textColor: "{colors.--base-color-text-default}" }
  base-select-option-disabled: { textColor: "{colors.--base-color-text-disabled}" }
  base-select-option-focus-visible: { backgroundColor: "{colors.--base-color-background-subtle}" }
  base-select-option-hover: { backgroundColor: "{colors.--base-color-background-subtle}" }
  base-select-option-selected: { backgroundColor: "{colors.--base-color-background-selected}" }
  base-select-popover: { backgroundColor: "{colors.--base-color-background-surface}", rounded: "{rounded.--base-radius-control}" }
  base-select-required: { textColor: "{colors.--base-color-status-danger-text}" }
  base-select-trigger: { backgroundColor: "{colors.--base-color-control-background}", rounded: "{rounded.--base-radius-control}", textColor: "{colors.--base-color-text-default}" }
  base-select-trigger-disabled: { backgroundColor: "{colors.--base-color-action-disabled-background}", textColor: "{colors.--base-color-text-disabled}" }
  base-select-trigger-hover: { backgroundColor: "{colors.--base-color-control-background-hover}" }
  base-side-nav-badge: { textColor: "{colors.--base-color-text-subtle}" }
  base-side-nav-link: { textColor: "{colors.--base-color-text-subtle}", rounded: "{rounded.--base-radius-control}" }
  base-side-nav-link-current: { textColor: "{colors.--base-color-text-default}", backgroundColor: "{colors.--base-color-background-selected}" }
  base-side-nav-link-hover: { backgroundColor: "{colors.--base-color-background-subtle}", textColor: "{colors.--base-color-text-default}" }
  base-sidenote: { textColor: "{colors.--base-color-text-subtle}" }
  base-sidenote-mark: { textColor: "{colors.--base-color-text-accent}" }
  base-skeleton-line1: { backgroundColor: "{colors.--base-color-background-subtle}", rounded: "{rounded.--base-radius-control}" }
  base-skeleton-line2: { backgroundColor: "{colors.--base-color-background-subtle}", rounded: "{rounded.--base-radius-control}", height: "12px" }
  base-skeleton-line3: { backgroundColor: "{colors.--base-color-background-subtle}", rounded: "{rounded.--base-radius-control}", height: "12px" }
  base-skip-link: { backgroundColor: "{colors.--base-color-background-inverse}", textColor: "{colors.--base-color-text-inverse}", rounded: "{rounded.--base-radius-control}" }
  base-sources-list-heading: { textColor: "{colors.--base-color-text-default}" }
  base-sources-list-link: { textColor: "{colors.--base-color-text-link}" }
  base-sources-list-provenance: { textColor: "{colors.--base-color-text-subtle}" }
  base-sources-list-publisher: { textColor: "{colors.--base-color-text-subtle}" }
  base-spinner-arc: { textColor: "{colors.--base-color-control-checked}" }
  base-spinner-label: { textColor: "{colors.--base-color-text-default}" }
  base-story-header-byline: { textColor: "{colors.--base-color-text-subtle}" }
  base-story-header-deck: { textColor: "{colors.--base-color-text-default}" }
  base-story-header-kicker: { textColor: "{colors.--base-color-text-accent}" }
  base-story-header-title: { textColor: "{colors.--base-color-text-default}" }
  base-streaming-text-body: { textColor: "{colors.--base-color-text-default}" }
  base-streaming-text-cursor: { backgroundColor: "{colors.--base-color-ai-border}" }
  base-streaming-text-status: { textColor: "{colors.--base-color-text-subtle}" }
  base-switch-hint: { textColor: "{colors.--base-color-text-subtle}" }
  base-switch-label: { textColor: "{colors.--base-color-text-default}" }
  base-switch-label-disabled: { textColor: "{colors.--base-color-text-disabled}" }
  base-switch-thumb: { width: "14px", height: "14px", rounded: "{rounded.--base-radius-pill}", backgroundColor: "{colors.--base-color-control-thumb}" }
  base-switch-thumb-checked: { backgroundColor: "{colors.--base-color-control-on-checked}" }
  base-switch-track: { width: "36px", height: "20px", rounded: "{rounded.--base-radius-pill}", backgroundColor: "{colors.--base-color-control-track}" }
  base-switch-track-checked: { backgroundColor: "{colors.--base-color-control-checked}" }
  base-switch-track-disabled: { backgroundColor: "{colors.--base-color-action-disabled-background}" }
  base-table: { backgroundColor: "{colors.--base-color-background-surface}" }
  base-table-cell: { textColor: "{colors.--base-color-text-default}" }
  base-table-column: { textColor: "{colors.--base-color-text-subtle}" }
  base-table-empty: { textColor: "{colors.--base-color-text-subtle}", padding: "{spacing.--base-space-inset-md}" }
  base-table-head: { backgroundColor: "{colors.--base-color-background-sunken}" }
  base-table-row-hover: { backgroundColor: "{colors.--base-color-background-subtle}" }
  base-table-row-selected: { backgroundColor: "{colors.--base-color-background-selected}" }
  base-tabs-panel: { textColor: "{colors.--base-color-text-default}" }
  base-tabs-tab: { textColor: "{colors.--base-color-text-subtle}", backgroundColor: "{colors.--base-color-action-quiet-background}" }
  base-tabs-tab-disabled: { textColor: "{colors.--base-color-text-disabled}" }
  base-tabs-tab-hover: { textColor: "{colors.--base-color-text-default}" }
  base-tabs-tab-selected: { textColor: "{colors.--base-color-text-default}" }
  base-tag-ai: { backgroundColor: "{colors.--base-color-ai-background}", textColor: "{colors.--base-color-ai-text}" }
  base-tag-info: { backgroundColor: "{colors.--base-color-status-info-background}", textColor: "{colors.--base-color-status-info-text}" }
  base-tag-neutral: { backgroundColor: "{colors.--base-color-status-neutral-background}", textColor: "{colors.--base-color-status-neutral-text}" }
  base-tag-remove-hover: { backgroundColor: "{colors.--base-color-background-subtle}" }
  base-text-area-error: { textColor: "{colors.--base-color-status-danger-text}" }
  base-text-area-hint: { textColor: "{colors.--base-color-text-subtle}" }
  base-text-area-input: { backgroundColor: "{colors.--base-color-control-background}", rounded: "{rounded.--base-radius-control}", textColor: "{colors.--base-color-text-default}" }
  base-text-area-input-disabled: { backgroundColor: "{colors.--base-color-action-disabled-background}", textColor: "{colors.--base-color-text-disabled}" }
  base-text-area-input-hover: { backgroundColor: "{colors.--base-color-control-background-hover}" }
  base-text-area-label: { textColor: "{colors.--base-color-text-default}" }
  base-text-area-required: { textColor: "{colors.--base-color-status-danger-text}" }
  base-text-danger: { textColor: "{colors.--base-color-status-danger-text}" }
  base-text-default: { textColor: "{colors.--base-color-text-default}" }
  base-text-field-error: { textColor: "{colors.--base-color-status-danger-text}" }
  base-text-field-hint: { textColor: "{colors.--base-color-text-subtle}" }
  base-text-field-input: { backgroundColor: "{colors.--base-color-control-background}", rounded: "{rounded.--base-radius-control}", textColor: "{colors.--base-color-text-default}" }
  base-text-field-input-disabled: { backgroundColor: "{colors.--base-color-action-disabled-background}", textColor: "{colors.--base-color-text-disabled}" }
  base-text-field-input-hover: { backgroundColor: "{colors.--base-color-control-background-hover}" }
  base-text-field-label: { textColor: "{colors.--base-color-text-default}" }
  base-text-field-required: { textColor: "{colors.--base-color-status-danger-text}" }
  base-text-subtle: { textColor: "{colors.--base-color-text-subtle}" }
  base-text-success: { textColor: "{colors.--base-color-status-success-text}" }
  base-toast-region-description: { textColor: "{colors.--base-color-text-inverse}" }
  base-toast-region-dismiss: { textColor: "{colors.--base-color-text-inverse}", rounded: "{rounded.--base-radius-control}" }
  base-toast-region-title: { textColor: "{colors.--base-color-text-inverse}" }
  base-toast-region-toast: { backgroundColor: "{colors.--base-color-background-inverse}", rounded: "{rounded.--base-radius-surface}", padding: "{spacing.--base-space-inset-sm}" }
  base-tooltip: { backgroundColor: "{colors.--base-color-background-inverse}", textColor: "{colors.--base-color-text-inverse}", rounded: "{rounded.--base-radius-control}" }
  base-warp-rail: { backgroundColor: "{colors.--base-color-field-ink-background}", width: "240px" }
  base-warp-rail-brandSub: { textColor: "{colors.--base-color-field-on-dark-text-subtle}" }
  base-warp-rail-brandTitle: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-warp-rail-knot-govern: { backgroundColor: "{colors.--base-color-thread-govern}" }
  base-warp-rail-knot-run: { backgroundColor: "{colors.--base-color-thread-run}" }
  base-warp-rail-knot-see: { backgroundColor: "{colors.--base-color-thread-see}" }
  base-warp-rail-knot-shape: { backgroundColor: "{colors.--base-color-thread-shape}" }
  base-warp-rail-mark: { width: "24px", height: "24px", textColor: "{colors.--base-color-field-on-dark-text}" }
  base-warp-rail-sectionLink: { textColor: "{colors.--base-color-field-on-dark-text}", rounded: "{rounded.--base-radius-control}" }
  base-warp-rail-sectionLink-current: { backgroundColor: "{colors.--base-color-field-on-dark-action}", textColor: "{colors.--base-color-field-on-dark-action-text}" }
  base-warp-rail-threadLink: { textColor: "{colors.--base-color-field-on-dark-text-subtle}", rounded: "{rounded.--base-radius-control}" }
  base-warp-rail-threadLink-current: { textColor: "{colors.--base-color-field-on-dark-text}" }
  base-weave-draft-cell: { width: "32px", height: "32px", backgroundColor: "{colors.--base-color-background-sunken}" }
  base-weave-draft-cell-selected: { backgroundColor: "{colors.--base-color-health-woven-ground}" }
  base-weave-draft-change: { textColor: "{colors.--base-color-text-default}" }
  base-weave-draft-column: { textColor: "{colors.--base-color-text-default}" }
  base-weave-draft-corner: { textColor: "{colors.--base-color-text-subtle}" }
  base-weave-draft-mark: { width: "20px", height: "20px", textColor: "{colors.--base-color-health-woven-thread}" }
---

# Loom Base

> Generated by Loom from the system's records; do not edit by hand. Regenerate with `npm run design-md -- --project loom-base`.
> Records: knowledge digest `a11d84b446fa`, source `ac29ae96a998`, observed build. 2312 tokens, 69 component contracts, 11 patterns in the snapshot; 80 governance records (real).
> Format: DESIGN.md alpha (github.com/google-labs-code/design.md). Token names are the system's own. Values are the default mode (light). "not defined" means the records hold no value; do not fill it in.
> The front matter's `components` entries group component-tier tokens named `<part>-<property>[-<state>]` (inferred from the names; the values are the records').

## Overview

Loom Base ships as `@loom/base` 1.0.0. Owner: base-core. Rights: Original work of the system owner.

- **principles** guidance: not defined (no principles foundation in the records).

### Token tiers and modes

Tiers are stated in the token records.

| Tier | Tokens |
|---|---|
| semantic | 213 |
| primitive | 311 |
| component | 1788 |

Modes: light (default), dark (65 tokens), high-contrast (75 tokens), field-dark (22 tokens), field-light (22 tokens), ja (10 tokens), compact (12 tokens), marked (1 token), reduced (3 tokens). The "Also set in" column names the modes that set a token again. Per-mode values are not defined in Loom's records; use the token (CSS custom property), which the system's theme resolves per mode.

## Colors

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-color-background-canvas` | #f3eee3 (`--base-color-palette-greige-25`) | dark, high-contrast | The page behind everything: unbleached cotton. Use for the app background, never for controls. |
| `--base-color-background-surface` | #fbf8f1 (`--base-color-palette-greige-0`) | dark, high-contrast | Sheets, panels, inputs and overlays that sit on the canvas: bleached cotton. |
| `--base-color-background-sunken` | #ebe4d6 (`--base-color-palette-greige-50`) | dark, high-contrast | Recessed areas: table headers, code, wells. Not for interactive fills. |
| `--base-color-background-subtle` | #e2d9c8 (`--base-color-palette-greige-100`) | dark, high-contrast | Hover fill for quiet controls and selected rows. |
| `--base-color-background-inverse` | #17140f (`--base-color-palette-greige-900`) | dark, high-contrast | Ink surfaces on a light page: tooltips, the skip link. Pair with color.text.inverse. |
| `--base-color-background-selected` | #eceefa (`--base-color-palette-dye-indigo-50`) | dark, high-contrast | Fill for a selected item in a list, menu or navigation. |
| `--base-color-background-backdrop` | #13110f7a (`--base-color-palette-scrim`) | dark | Scrim behind modal dialogs and drawers. Never for content. |
| `--base-color-text-default` | #17140f (`--base-color-palette-greige-900`) | dark, high-contrast, field-dark, field-light | Loom-black ink. Body text and headings on canvas and surface. |
| `--base-color-text-subtle` | #4a443a (`--base-color-palette-greige-600`) | dark, high-contrast, field-dark, field-light | Secondary text: hints, captions, metadata. Still meets 4.5:1 on canvas and surface. |
| `--base-color-text-disabled` | #8f8571 (`--base-color-palette-greige-400`) | dark, high-contrast | Text of disabled controls only. Exempt from contrast (WCAG 1.4.3); never for readable content. |
| `--base-color-text-inverse` | #f3eee3 (`--base-color-palette-greige-25`) | dark, high-contrast | Text on color.background.inverse. |
| `--base-color-text-link` | #1e2a78 (`--base-color-palette-dye-indigo-800`) | dark, high-contrast, field-dark, field-light | Indigo: links and anything that goes somewhere. |
| `--base-color-text-link-hover` | #151d55 (`--base-color-palette-dye-indigo-900`) | dark, high-contrast, field-dark, field-light | Link under the pointer. |
| `--base-color-text-accent` | #b02a1e (`--base-color-palette-dye-madder-600`) | dark, high-contrast, field-dark, field-light | Madder: eyebrows, the current marker on a scale, accent words. Decorative emphasis, never the only signal. |
| `--base-color-border-default` | #d1c6b2 (`--base-color-palette-greige-200`) | dark, high-contrast, field-dark, field-light | Hairlines between rows and regions. Not enough contrast to identify a control. |
| `--base-color-border-strong` | #716856 (`--base-color-palette-greige-500`) | dark, high-contrast, field-dark, field-light | Boundaries that identify a control (inputs, checkboxes, secondary buttons). Meets 3:1 against surface. |
| `--base-color-border-inverse` | #17140f (`--base-color-palette-greige-900`) | dark, high-contrast | Rules that must read as ink, e.g. the active tab indicator in high contrast. |
| `--base-color-border-accent` | #b02a1e (`--base-color-palette-dye-madder-600`) | dark, high-contrast, field-dark, field-light | The shu thread: the marker under the current section and the rule of AI-free accents. Non-text, meets 3:1 on canvas and surface. Never for status or actions. |
| `--base-color-border-rule` | #17140f (`--base-color-palette-greige-900`) | dark, high-contrast, field-dark, field-light | Heavy rules: under page heads, above columns and the cloth. |
| `--base-color-action-primary-background` | #17140f (`--base-color-palette-greige-900`) | dark, high-contrast, field-dark, field-light | Primary action fill: loom-black ink. Colour is kept for meaning (dyes mark threads and health bands). |
| `--base-color-action-primary-background-hover` | #38332b (`--base-color-palette-greige-700`) | dark, high-contrast, field-dark, field-light | Primary action under the pointer. |
| `--base-color-action-primary-background-pressed` | #4a443a (`--base-color-palette-greige-600`) | dark, high-contrast, field-dark, field-light | Primary action while pressed. |
| `--base-color-action-primary-text` | #fbf8f1 (`--base-color-palette-greige-0`) | dark, high-contrast, field-dark, field-light | Label on the primary fill. |
| `--base-color-action-secondary-background` | #fbf8f1 (`--base-color-palette-greige-0`) | dark, field-dark, field-light | Fill for secondary actions. |
| `--base-color-action-secondary-background-hover` | #ebe4d6 (`--base-color-palette-greige-50`) | dark, high-contrast, field-dark, field-light | Secondary action under the pointer. |
| `--base-color-action-secondary-background-pressed` | #e2d9c8 (`--base-color-palette-greige-100`) | dark, high-contrast, field-dark, field-light | Secondary action while pressed. |
| `--base-color-action-secondary-text` | #17140f (`--base-color-palette-greige-900`) | dark, high-contrast, field-dark, field-light | Label of secondary actions. |
| `--base-color-action-secondary-border` | #17140f (`--base-color-palette-greige-900`) | dark, high-contrast, field-dark, field-light | Outline of secondary actions: an ink rule (well above 3:1). |
| `--base-color-action-quiet-background` | #00000000 (`--base-color-palette-transparent`) | — | Quiet actions in toolbars and dense areas have no fill at rest. |
| `--base-color-action-quiet-background-hover` | #ebe4d6 (`--base-color-palette-greige-50`) | dark, high-contrast, field-dark, field-light | Quiet action under the pointer. |
| `--base-color-action-quiet-background-pressed` | #e2d9c8 (`--base-color-palette-greige-100`) | dark, high-contrast, field-dark, field-light | Quiet action while pressed. |
| `--base-color-action-quiet-text` | #17140f (`--base-color-palette-greige-900`) | dark, high-contrast, field-dark, field-light | Label of quiet actions. |
| `--base-color-action-danger-background` | #a0312a (`--base-color-palette-red-600`) | dark, high-contrast | Fill for an action that removes or loses something. Always paired with a confirmation. |
| `--base-color-action-danger-background-hover` | #7f1814 (`--base-color-palette-red-700`) | dark, high-contrast | Danger action under the pointer. |
| `--base-color-action-danger-background-pressed` | #600204 (`--base-color-palette-red-800`) | dark, high-contrast | Danger action while pressed. |
| `--base-color-action-danger-text` | #fefdfb (`--base-color-palette-neutral-0`) | dark, high-contrast | Label on the danger fill. |
| `--base-color-action-disabled-background` | #e2d9c8 (`--base-color-palette-greige-100`) | dark, high-contrast | Fill of any disabled action. |
| `--base-color-action-disabled-border` | #d1c6b2 (`--base-color-palette-greige-200`) | dark, high-contrast | Outline of any disabled control. |
| `--base-color-control-background` | #fbf8f1 (`--base-color-palette-greige-0`) | dark, high-contrast | Field background: inputs, selects, checkboxes. |
| `--base-color-control-background-hover` | #f3eee3 (`--base-color-palette-greige-25`) | dark, high-contrast | Field under the pointer. |
| `--base-color-control-checked` | #1e2a78 (`--base-color-palette-dye-indigo-800`) | dark, high-contrast | Fill of a checked checkbox, radio dot or switch track: indigo. |
| `--base-color-control-on-checked` | #fbf8f1 (`--base-color-palette-greige-0`) | dark, high-contrast | Check mark and thumb on color.control.checked. |
| `--base-color-control-track` | #d1c6b2 (`--base-color-palette-greige-200`) | dark, high-contrast | Unfilled track of a progress bar or an off switch, with color.border.strong as its edge. |
| `--base-color-control-thumb` | #fbf8f1 (`--base-color-palette-greige-0`) | dark, high-contrast | Switch thumb when off. |
| `--base-color-focus-ring` | #1e2a78 (`--base-color-palette-dye-indigo-800`) | dark, high-contrast, field-dark, field-light | Focus ring colour on every interactive element: indigo on the page, cotton or ink inside colour fields (surface modifier). Locked: lower layers may not change it (WCAG 2.4.7, 2.4.13). |
| `--base-color-status-info-background` | #e8f1ff (`--base-color-palette-indigo-50`) | dark | Soft fill of informational messages. |
| `--base-color-status-info-border` | #5478ba (`--base-color-palette-indigo-500`) | dark, high-contrast | Edge of informational messages. |
| `--base-color-status-info-text` | #152e5e (`--base-color-palette-indigo-800`) | dark, high-contrast | Text and icon in informational messages. |
| `--base-color-status-success-background` | #d5fcde (`--base-color-palette-green-50`) | dark | Soft fill of success messages. |
| `--base-color-status-success-border` | #3f8b58 (`--base-color-palette-green-500`) | dark, high-contrast | Edge of success messages. |
| `--base-color-status-success-text` | #003b1a (`--base-color-palette-green-800`) | dark, high-contrast | Text and icon in success messages. |
| `--base-color-status-warning-background` | #ffedd8 (`--base-color-palette-amber-50`) | dark | Soft fill of warnings. |
| `--base-color-status-warning-border` | #a46b0c (`--base-color-palette-amber-500`) | dark, high-contrast | Edge of warnings. |
| `--base-color-status-warning-text` | #452a00 (`--base-color-palette-amber-800`) | dark, high-contrast | Text and icon in warnings. |
| `--base-color-status-danger-background` | #ffebe8 (`--base-color-palette-red-50`) | dark | Soft fill of errors. |
| `--base-color-status-danger-border` | #a0312a (`--base-color-palette-red-600`) | dark, high-contrast | Edge of errors and invalid fields. |
| `--base-color-status-danger-text` | #7f1814 (`--base-color-palette-red-700`) | dark, high-contrast | Error text, error icons and the invalid-field message. |
| `--base-color-status-neutral-background` | #e8e6e1 (`--base-color-palette-neutral-100`) | dark | Soft fill of neutral badges and tags. |
| `--base-color-status-neutral-border` | #c0bdb8 (`--base-color-palette-neutral-300`) | dark, high-contrast | Edge of neutral badges and tags. |
| `--base-color-status-neutral-text` | #32302c (`--base-color-palette-neutral-800`) | dark, high-contrast | Text of neutral badges and tags. |
| `--base-color-ai-background` | #f2edff (`--base-color-palette-violet-50`) | dark | Fill that marks AI-generated content or an AI label. Locked so AI disclosure stays recognisable (EU AI Act Art. 50). |
| `--base-color-ai-border` | #826bab (`--base-color-palette-violet-500`) | dark, high-contrast | Edge of AI-generated regions and the AI label. |
| `--base-color-ai-text` | #4d386f (`--base-color-palette-violet-700`) | dark, high-contrast | Text of the AI label and AI status. |
| `--base-color-field-indigo-background` | #1e2a78 (`--base-color-palette-dye-indigo-800`) | high-contrast | Indigo field (See, Tightly woven). A dark field: field-dark surface. |
| `--base-color-field-madder-background` | #b02a1e (`--base-color-palette-dye-madder-600`) | high-contrast | Madder field (Shape, Loose threads). A dark field: field-dark surface. |
| `--base-color-field-saffron-background` | #e9a21b (`--base-color-palette-dye-saffron-400`) | — | Saffron field (Govern, Taking shape). A light field: field-light surface, ink text. |
| `--base-color-field-teal-background` | #0b6b66 (`--base-color-palette-dye-teal-600`) | high-contrast | Teal field (Run, Holding together). A dark field: field-dark surface. |
| `--base-color-field-ink-background` | #17140f (`--base-color-palette-greige-900`) | dark, high-contrast | Ink field: the warp rail. A dark field: field-dark surface. |
| `--base-color-field-on-dark-text` | #f3eee3 (`--base-color-palette-greige-25`) | high-contrast | Text on dark fields (indigo, madder, teal, ink). |
| `--base-color-field-on-dark-text-subtle` | #e2d9c8 (`--base-color-palette-greige-100`) | high-contrast | Secondary text and eyebrows on dark fields; 4.5:1 on each. |
| `--base-color-field-on-dark-border` | #f3eee3 (`--base-color-palette-greige-25`) | high-contrast | Borders and outline buttons on dark fields. |
| `--base-color-field-on-dark-focus` | #f3eee3 (`--base-color-palette-greige-25`) | high-contrast | Focus ring on dark fields: cotton. |
| `--base-color-field-on-dark-action` | #fbf8f1 (`--base-color-palette-greige-0`) | — | Primary action fill on dark fields. |
| `--base-color-field-on-dark-action-hover` | #ffffff (`--base-color-palette-white`) | — | Primary action fill under the pointer on dark fields. |
| `--base-color-field-on-dark-action-pressed` | #e2d9c8 (`--base-color-palette-greige-100`) | — | Pressed primary action on dark fields. |
| `--base-color-field-on-dark-action-text` | #17140f (`--base-color-palette-greige-900`) | — | Text on the primary action on dark fields. |
| `--base-color-field-on-light-text` | #17140f (`--base-color-palette-greige-900`) | high-contrast | Text on light fields (saffron). |
| `--base-color-field-on-light-text-subtle` | #26221c (`--base-color-palette-greige-800`) | high-contrast | Secondary text and eyebrows on light fields. |
| `--base-color-field-on-light-border` | #17140f (`--base-color-palette-greige-900`) | high-contrast | Borders and outline buttons on light fields. |
| `--base-color-field-on-light-focus` | #17140f (`--base-color-palette-greige-900`) | high-contrast | Focus ring on light fields: ink. |
| `--base-color-field-on-light-action` | #17140f (`--base-color-palette-greige-900`) | — | Primary action fill on light fields. |
| `--base-color-field-on-light-action-hover` | #38332b (`--base-color-palette-greige-700`) | — | Primary action fill under the pointer on light fields. |
| `--base-color-field-on-light-action-pressed` | #4a443a (`--base-color-palette-greige-600`) | — | Pressed primary action on light fields. |
| `--base-color-field-on-light-action-text` | #fbf8f1 (`--base-color-palette-greige-0`) | — | Text on the primary action on light fields. |
| `--base-color-thread-see` | #6f7fe0 (`--base-color-palette-dye-indigo-400`) | — | See thread: indigo. |
| `--base-color-thread-shape` | #d4574a (`--base-color-palette-dye-madder-400`) | — | Shape thread: madder. |
| `--base-color-thread-govern` | #e9a21b (`--base-color-palette-dye-saffron-400`) | — | Govern thread: saffron. |
| `--base-color-thread-run` | #1d948c (`--base-color-palette-dye-teal-400`) | — | Run thread: teal. |
| `--base-color-thread-slack` | #38332b (`--base-color-palette-greige-700`) | — | Unlit thread lines from a section to its warp thread. |
| `--base-color-health-woven-thread` | #1e2a78 (`--base-color-palette-dye-indigo-800`) | high-contrast | Tightly woven: indigo thread. |
| `--base-color-health-woven-ground` | #dde1f6 (`--base-color-palette-dye-indigo-100`) | — | Tightly woven: ground. |
| `--base-color-health-holding-thread` | #0b6b66 (`--base-color-palette-dye-teal-600`) | high-contrast | Holding together: teal thread. |
| `--base-color-health-holding-ground` | #c5e6e2 (`--base-color-palette-dye-teal-100`) | — | Holding together: ground. |
| `--base-color-health-shape-thread` | #9e6b08 (`--base-color-palette-dye-saffron-600`) | high-contrast | Taking shape: saffron thread (deepened to 3.9:1 on cotton). |
| `--base-color-health-shape-ground` | #fbe7bd (`--base-color-palette-dye-saffron-100`) | — | Taking shape: ground. |
| `--base-color-health-loose-thread` | #b02a1e (`--base-color-palette-dye-madder-600`) | high-contrast | Loose threads: madder thread. |
| `--base-color-health-loose-ground` | #fbecea (`--base-color-palette-dye-madder-50`) | — | Loose threads: ground. |
| `--base-color-health-chip-background` | #fbf8f1 (`--base-color-palette-greige-0`) | high-contrast | Label chip on a cloth cell: bleached cotton in every mode, because cells keep their light grounds in dark mode. |
| `--base-color-health-chip-text` | #17140f (`--base-color-palette-greige-900`) | high-contrast | Text on the cloth label chip, in every mode. |

Primitive tier (181):

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-color-palette-scrim` | #13110f7a | — | Backdrop behind modal overlays. |
| `--base-color-palette-scrim-strong` | #000000a8 | — | Backdrop in dark mode. |

- `--base-color-palette-neutral-*`: `0` #fefdfb · `25` #f9f7f2 · `50` #f2f0eb · `100` #e8e6e1 · `200` #dbd9d4 · `300` #c0bdb8 · `400` #9b9892 · `500` #7b7873 · `600` #605d58 · `700` #474540 · `800` #32302c · `900` #201e1a · `950` #13110e
- `--base-color-palette-indigo-*`: `50` #e8f1ff · `100` #d9e7ff · `200` #c5daff · `300` #9ebef8 · `400` #7598d9 · `500` #5478ba · `600` #3b5c9a · `700` #25437b · `800` #152e5e · `900` #081b42 · `950` #030f2e
- `--base-color-palette-green-*`: `50` #d5fcde · `100` #c8f3d2 · `200` #b6e8c2 · `300` #92cfa2 · `400` #64ab78 · `500` #3f8b58 · `600` #216e3e · `700` #025428 · `800` #003b1a · `900` #00260e · `950` #001706
- `--base-color-palette-amber-*`: `50` #ffedd8 · `100` #ffe1bd · `200` #fad19f · `300` #e5b476 · `400` #c38c40 · `500` #a46b0c · `600` #815200 · `700` #603c00 · `800` #452a00 · `900` #2d1900 · `950` #1c0e00
- `--base-color-palette-red-*`: `50` #ffebe8 · `100` #ffddd8 · `200` #ffcbc3 · `300` #ffa194 · `400` #e37265 · `500` #c14f44 · `600` #a0312a · `700` #7f1814 · `800` #600204 · `900` #410001 · `950` #2b0000
- `--base-color-palette-violet-*`: `50` #f2edff · `100` #eae0ff · `200` #dfd0ff · `300` #c5b2eb · `400` #a18bca · `500` #826bab · `600` #66508d · `700` #4d386f · `800` #372553 · `900` #24143a · `950` #160927
- `--base-color-palette-greige-*`: `0` #fbf8f1 · `25` #f3eee3 · `50` #ebe4d6 · `100` #e2d9c8 · `200` #d1c6b2 · `300` #b3a892 · `400` #8f8571 · `500` #716856 · `600` #4a443a · `700` #38332b · `800` #26221c · `900` #17140f · `950` #0d0b08
- `--base-color-palette-dye-indigo-*`: `50` #eceefa · `100` #dde1f6 · `200` #c3c9ef · `300` #9aa5e6 · `400` #6f7fe0 · `500` #4f5fc4 · `600` #3a49a8 · `700` #2f3d97 · `800` #1e2a78 · `900` #151d55 · `950` #0d1238
- `--base-color-palette-dye-madder-*`: `50` #fbecea · `100` #f7d8d4 · `200` #efb3ab · `300` #e3877a · `400` #d4574a · `500` #c9473a · `600` #b02a1e · `700` #8f2016 · `800` #6e1810 · `900` #4c110b
- `--base-color-palette-dye-saffron-*`: `50` #fdf4e1 · `100` #fbe7bd · `200` #f7d48a · `300` #f3bb4c · `400` #e9a21b · `500` #c98a0f · `600` #9e6b08 · `700` #7a5206 · `800` #573a04 · `900` #3a2603
- `--base-color-palette-dye-teal-*`: `50` #e3f3f1 · `100` #c5e6e2 · `200` #94d1ca · `300` #5fb6ad · `400` #1d948c · `500` #138a83 · `600` #0b6b66 · `700` #08544f · `800` #063e3b · `900` #042a28
- `--base-color-palette-cotton-*`: `0` #fffdf8 · `25` #f8f4ea · `50` #f1ece0 · `100` #e7e0d6 · `200` #dfdad1 · `300` #b6b0a9 · `400` #848992 · `500` #666c79 · `600` #4e5565 · `700` #374051 · `800` #232b3e · `900` #10182b · `950` #070e1d
- `--base-color-palette-aizome-*`: `50` #e6efff · `100` #d2e2ff · `200` #b4cfff · `300` #8fb4fb · `400` #628eed · `500` #3f6bdc · `600` #2649c0 · `700` #1d369e · `800` #142779 · `900` #0d1c58 · `950` #07113a
- `--base-color-palette-kaki-*`: `200` #f8caa7 · `300` #f5ab77 · `400` #ee8545 · `500` #de6420 · `600` #c04d15 · `700` #9f3d15 · `800` #7a3018
- `--base-color-palette-washi-*`: `0` #fffcf5 · `25` #f6f1e7 · `50` #eee7da · `100` #e5ddcd · `200` #cfc6b5 · `300` #ada492 · `400` #8a8273 · `500` #6e675b · `600` #4d473f · `700` #38332d · `800` #26231f · `900` #141210 · `950` #0b0a08
- `--base-color-palette-shu-*`: `50` #fcede8 · `100` #f9d9cf · `200` #f3b5a3 · `300` #ec9077 · `400` #e06a4d · `500` #cf4b2c · `600` #c0341d · `700` #9e2a16 · `800` #7b2212 · `900` #55190e
- `--base-color-palette-*`: `white` #ffffff · `black` #000000 · `transparent` #00000000

Component tier: 592 tokens, listed per component under Components.

### Contrast pairs

Contrast pairs: not defined.

### Guidance

- **Color** guidance: not defined (foundation status: partial).
- Gap recorded for Color: Guidance per foundation lives in docs/tokens.md and the token descriptions; no foundation page yet.

## Typography

### Font families

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-font-family-sans` | 'Atkinson Hyperlegible Next', 'Noto Sans JP', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic UI', 'Yu Gothic', Meiryo, system-ui, sans-serif (`--base-font-family-stack-interface`) | ja | Interface text. Swapped for the Japanese-first stack under :lang(ja). |
| `--base-font-family-mono` | 'Martian Mono', ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace (`--base-font-family-stack-figure`) | — | Code, ids and tabular figures. |
| `--base-font-family-display` | Archivo, 'Noto Sans JP', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic UI', 'Yu Gothic', Meiryo, system-ui, sans-serif (`--base-font-family-stack-display`) | ja | Display face (Archivo, set wide and black, in the default Selvedge theme; Fraunces in Atelier Press) for health-band headlines, page titles, headlines, numerals and empty-state titles. Not for body text, labels or controls. |
| `--base-font-family-overline` | 'Martian Mono', ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace (`--base-font-family-stack-figure`) | ja | Eyebrows and kickers above titles (Martian Mono in the default Selvedge theme; IBM Plex Mono in Atelier Press). Short uppercase labels only. |

Primitive tier (14):

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-font-family-stack-latin` | 'Helvetica Neue', 'Segoe UI', Arial, 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', 'Yu Gothic', Meiryo, system-ui, sans-serif | — | Latin-first stack with Japanese fallbacks. System fonts only: no font files ship with Base. |
| `--base-font-family-stack-japanese` | 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', 'Yu Gothic UI', 'Yu Gothic', Meiryo, 'Helvetica Neue', Arial, system-ui, sans-serif | — | Japanese-first stack (system Japanese fonts) used under :lang(ja). |
| `--base-font-family-stack-interface` | 'Atkinson Hyperlegible Next', 'Noto Sans JP', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic UI', 'Yu Gothic', Meiryo, system-ui, sans-serif | — | Default theme (Selvedge) interface face: Atkinson Hyperlegible Next (OFL, shipped in fonts/), drawn for legibility at small sizes, then Noto Sans JP (the Japanese default, OFL). |
| `--base-font-family-stack-display` | Archivo, 'Noto Sans JP', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic UI', 'Yu Gothic', Meiryo, system-ui, sans-serif | — | Default theme (Selvedge) display face: Archivo (OFL, shipped in fonts/; variable weight and width). The Provider sets font-stretch 125%, so Archivo is set wide; faces without a width axis ignore it. Japanese glyphs fall back to Noto Sans JP. |
| `--base-font-family-stack-figure` | 'Martian Mono', ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace | — | Default theme (Selvedge) mono face: Martian Mono (OFL, shipped in fonts/) for eyebrows, figures and code. |
| `--base-font-family-stack-schibsted-grotesk` | 'Schibsted Grotesk', 'Noto Sans JP', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic UI', 'Yu Gothic', Meiryo, 'Helvetica Neue', Arial, system-ui, sans-serif | — | Atelier Press (alternative theme) interface face: Schibsted Grotesk (OFL, shipped in fonts/), a face drawn for a news publisher, then Noto Sans JP (the Japanese default, OFL) and system Japanese fonts for kana and kanji. |
| `--base-font-family-stack-interface-japanese` | 'Noto Sans JP', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic UI', 'Yu Gothic', Meiryo, 'Atkinson Hyperlegible Next', system-ui, sans-serif | — | Interface stack under :lang(ja): Noto Sans JP (the Japanese default, OFL; owner decision 25 Oct 2026) for Japanese and Latin alike, then system Japanese fonts. |
| `--base-font-family-stack-fraunces` | Fraunces, 'Noto Sans JP', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic UI', 'Yu Gothic', Meiryo, Georgia, serif | — | Atelier Press (alternative theme) display face: Fraunces (OFL, shipped in fonts/; its optical size axis sharpens it as it grows) for mastheads, page titles, headlines and numerals. Japanese glyphs fall back to Noto Sans JP. |
| `--base-font-family-stack-display-japanese` | 'Noto Sans JP', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic UI', 'Yu Gothic', Meiryo, sans-serif | — | Display stack under :lang(ja): Noto Sans JP, so Japanese headings stay in the Japanese default face; figures and Latin runs inside Japanese text follow it. |
| `--base-font-family-stack-ibm-plex-mono` | 'IBM Plex Mono', ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace | — | Atelier Press (alternative theme) dateline and code face: IBM Plex Mono (OFL, shipped in fonts/). Kickers, datelines, token names, code. |
| `--base-font-family-stack-instrument-sans` | 'Instrument Sans', 'Noto Sans JP', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic UI', 'Yu Gothic', Meiryo, 'Helvetica Neue', Arial, system-ui, sans-serif | — | Kasuri interface face: Instrument Sans (OFL, shipped in fonts/), then Noto Sans JP and system Japanese fonts. |
| `--base-font-family-stack-instrument-serif` | 'Instrument Serif', 'Noto Sans JP', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic UI', 'Yu Gothic', Meiryo, Georgia, serif | — | Kasuri display face: Instrument Serif (OFL, shipped in fonts/). Japanese glyphs fall back to Noto Sans JP. |
| `--base-font-family-stack-jetbrains-mono` | 'JetBrains Mono', ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace | — | Kasuri figure and code face: JetBrains Mono (OFL, shipped in fonts/). |

- `--base-font-family-stack-mono` ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace

Component tier: 158 tokens, listed per component under Components.

### Font sizes

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-font-size-display` | 64px (`--base-font-size-scale-64`) | — | Marketing and empty-page hero only. One per page. |
| `--base-font-size-headline-lg` | 36px (`--base-font-size-scale-36`) | — | Page title on wide layouts. |
| `--base-font-size-headline-md` | 30px (`--base-font-size-scale-30`) | — | Page title (h1) in the app. |
| `--base-font-size-headline-sm` | 24px (`--base-font-size-scale-24`) | — | Section heading (h2). |
| `--base-font-size-title` | 20px (`--base-font-size-scale-20`) | — | Card, dialog and panel titles (h3). |
| `--base-font-size-subtitle` | 18px (`--base-font-size-scale-18`) | — | Sub-section heading (h4). |
| `--base-font-size-body-lg` | 18px (`--base-font-size-scale-18`) | — | Lead paragraphs and story decks. |
| `--base-font-size-body` | 16px (`--base-font-size-scale-16`) | compact | Default interface and reading text. |
| `--base-font-size-body-sm` | 14px (`--base-font-size-scale-14`) | — | Dense tables and secondary text. |
| `--base-font-size-label` | 14px (`--base-font-size-scale-14`) | compact | Field labels and control text. |
| `--base-font-size-caption` | 13px (`--base-font-size-scale-13`) | — | Hints, metadata and badges. Never below this size for readable text. |
| `--base-font-size-overline` | 12px (`--base-font-size-scale-12`) | — | Eyebrows in uppercase Latin only; not used for Japanese text. |
| `--base-font-size-page-title` | 56px (`--base-font-size-scale-56`) | — | Page titles in the PageHeader and section heads: Archivo, wide and black. |
| `--base-font-size-figure` | 72px (`--base-font-size-scale-72`) | — | Figures on colour-block cards and key numbers. |
| `--base-font-size-masthead` | 72px (`--base-font-size-scale-72`) | — | The publication title in Masthead. One per page; wraps onto two lines on a phone. |
| `--base-font-size-numeral` | 120px (`--base-font-size-scale-120`) | — | The one hero figure on a front page (Numeral). Always with a text label and caption. |
| `--base-font-size-band` | 96px (`--base-font-size-scale-96`) | — | The health-band headline ("Holding together."): Archivo, wide and black. Smaller on narrow screens. |

Primitive tier (17):

- `--base-font-size-scale-*`: `12` 12px · `13` 13px · `14` 14px · `15` 15px · `16` 16px · `18` 18px · `20` 20px · `24` 24px · `30` 30px · `36` 36px · `44` 44px · `48` 48px · `56` 56px · `64` 64px · `72` 72px · `96` 96px · `120` 120px

Component tier: 162 tokens, listed per component under Components.

### Font weights

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-font-weight-regular` | 400 (`--base-font-weight-scale-regular`) | — | Body text and large headings (the calm look uses light headings). |
| `--base-font-weight-medium` | 500 (`--base-font-weight-scale-medium`) | — | Labels, buttons and small headings. |
| `--base-font-weight-strong` | 600 (`--base-font-weight-scale-semibold`) | — | Emphasis inside text and numbers that must stand out. Use sparingly. |
| `--base-font-weight-display` | 900 (`--base-font-weight-scale-black`) | — | Display weight: Archivo Black (900) for headlines, page titles and figures. |
| `--base-font-weight-numeral` | 900 (`--base-font-weight-scale-black`) | — | Weight of Numeral figures: heavy, so a single number can carry a page. |

Primitive tier (6):

- `--base-font-weight-scale-*`: `regular` 400 · `medium` 500 · `semibold` 600 · `bold` 700 · `heavy` 800 · `black` 900

Component tier: 103 tokens, listed per component under Components.

### Line heights

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-font-line-height-tight` | 1.2 (`--base-font-line-height-scale-1-2`) | ja | Display and headline sizes. |
| `--base-font-line-height-heading` | 1.3 (`--base-font-line-height-scale-1-3`) | ja | Titles and subtitles. |
| `--base-font-line-height-control` | 1.4 (`--base-font-line-height-scale-1-4`) | ja | Single-line controls: buttons, tags, inputs. |
| `--base-font-line-height-body` | 1.6 (`--base-font-line-height-scale-1-6`) | ja | Paragraphs and multi-line text. Raised for Japanese. |
| `--base-font-line-height-display` | 0.9 (`--base-font-line-height-scale-0-9`) | — | Page titles, headlines and numerals in the display face: set solid. |

Primitive tier (11):

- `--base-font-line-height-scale-0-9` 0.9
- `--base-font-line-height-scale-1-*`: `0` 1 · `1` 1.1 · `2` 1.2 · `3` 1.3 · `4` 1.4 · `5` 1.5 · `6` 1.6 · `7` 1.7 · `8` 1.8 · `9` 1.9

Component tier: 113 tokens, listed per component under Components.

### Letter spacing

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-font-letter-spacing-heading` | -0.02 (`--base-font-letter-spacing-scale-tight`) | ja | Large Latin headings. Japanese never uses negative tracking. |
| `--base-font-letter-spacing-body` | 0 (`--base-font-letter-spacing-scale-none`) | ja | Body and control text. |
| `--base-font-letter-spacing-overline` | 0.12 (`--base-font-letter-spacing-scale-wider`) | — | Eyebrows and labels in Martian Mono, set in capitals with open tracking. |
| `--base-font-letter-spacing-display` | -0.02 (`--base-font-letter-spacing-scale-tight`) | ja | Tracking for the display face at page-title and figure sizes. |
| `--base-font-letter-spacing-masthead` | -0.03 (`--base-font-letter-spacing-scale-tighter`) | — | Tracking of the masthead title. Latin only; Japanese never uses negative tracking. |

Primitive tier (7):

- `--base-font-letter-spacing-scale-*`: `tighter` -0.03 · `tight` -0.02 · `snug` -0.01 · `none` 0 · `cjk` 0.02 · `wide` 0.08 · `wider` 0.12

Component tier: 4 tokens, listed per component under Components.

### Other type tokens

Component tier: 20 tokens, listed per component under Components.

### Guidance

- **Typography** guidance: not defined (foundation status: partial).
- Gap recorded for Typography: Guidance per foundation lives in docs/tokens.md and the token descriptions; no foundation page yet.

## Layout

### Spacing

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-space-inset-xs` | 4px (`--base-space-scale-2`) | — | Padding inside tags and badges. |
| `--base-space-inset-sm` | 8px (`--base-space-scale-4`) | compact | Padding inside compact controls and menu items. |
| `--base-space-inset-md` | 16px (`--base-space-scale-6`) | compact | Padding inside cards and alerts. |
| `--base-space-inset-lg` | 24px (`--base-space-scale-8`) | compact | Padding inside dialogs and page sections. |
| `--base-space-inset-control-x` | 16px (`--base-space-scale-6`) | compact | Horizontal padding of buttons and inputs. |
| `--base-space-stack-xs` | 4px (`--base-space-scale-2`) | — | Gap between a label and its control, or a title and its hint. |
| `--base-space-stack-sm` | 8px (`--base-space-scale-4`) | compact | Gap between related items in a group. |
| `--base-space-stack-md` | 16px (`--base-space-scale-6`) | compact | Gap between fields in a form. |
| `--base-space-stack-lg` | 24px (`--base-space-scale-8`) | compact | Gap between groups within a section. |
| `--base-space-stack-xl` | 40px (`--base-space-scale-10`) | — | Gap between page sections. |
| `--base-space-inline-xs` | 4px (`--base-space-scale-2`) | — | Gap between an icon and its label. |
| `--base-space-inline-sm` | 8px (`--base-space-scale-4`) | — | Gap between adjacent buttons or tags. |
| `--base-space-inline-md` | 16px (`--base-space-scale-6`) | — | Gap between toolbar groups. |
| `--base-space-inline-lg` | 24px (`--base-space-scale-8`) | — | Gap between header regions. |
| `--base-space-layout-gutter` | 16px (`--base-space-scale-6`) | — | Page side gutter on narrow screens and grid gutter. |
| `--base-space-layout-section` | 48px (`--base-space-scale-11`) | — | Vertical rhythm between major page regions. |
| `--base-space-weave` | 4px (`--base-space-scale-2`) | — | The gap between cloth and weaving-draft cells, and inside a cell chip: one thread width. |

Primitive tier (14):

- `--base-space-scale-*`: `0` 0px · `1` 2px · `2` 4px · `3` 6px · `4` 8px · `5` 12px · `6` 16px · `7` 20px · `8` 24px · `9` 32px · `10` 40px · `11` 48px · `12` 64px · `13` 80px

Component tier: 115 tokens, listed per component under Components.

### Sizes, containers and breakpoints

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-size-control-sm` | 32px (`--base-size-scale-32`) | compact | Height of small controls in dense toolbars and tables. |
| `--base-size-control-md` | 40px (`--base-size-scale-40`) | compact | Default height of buttons, inputs and selects. |
| `--base-size-control-lg` | 48px (`--base-size-scale-48`) | compact | Prominent controls on sign-in and empty pages. |
| `--base-size-target-min` | 24px (`--base-size-scale-24`) | — | Minimum pointer target (WCAG 2.5.8). Locked: no layer may go below it. |
| `--base-size-icon-sm` | 16px (`--base-size-scale-16`) | — | Icons inside small controls and text. |
| `--base-size-icon-md` | 20px (`--base-size-scale-20`) | — | Default icon size. |
| `--base-size-icon-lg` | 24px (`--base-size-scale-24`) | — | Icons in empty states and alerts. |
| `--base-size-toggle-width` | 36px (`--base-size-scale-36`) | — | Switch track width. |
| `--base-size-toggle-height` | 20px (`--base-size-scale-20`) | — | Switch track height. |
| `--base-size-toggle-thumb` | 14px (`--base-size-scale-14`) | — | Switch thumb diameter. |
| `--base-size-toggle-box` | 16px (`--base-size-scale-16`) | — | Checkbox and radio box size; the hit area is padded to size.target.min. |
| `--base-size-progress-track` | 6px (`--base-size-scale-6`) | — | Height of the progress bar track. |
| `--base-size-skeleton-line` | 12px (`--base-size-scale-12`) | — | Height of one skeleton text line. |
| `--base-size-avatar-sm` | 24px (`--base-size-scale-24`) | — | Avatar in dense lists. |
| `--base-size-avatar-md` | 32px (`--base-size-scale-32`) | — | Default avatar. |
| `--base-size-avatar-lg` | 48px (`--base-size-scale-48`) | — | Avatar on profile headers. |
| `--base-size-border-thin` | 1px (`--base-size-scale-1`) | high-contrast | Rules, card and field borders. |
| `--base-size-border-thick` | 2px (`--base-size-scale-2`) | high-contrast | Selected indicators and the invalid-field edge. |
| `--base-size-border-ai` | 1px (`--base-size-scale-1`) | marked | Edge width of AI-generated regions and the AI label. The ai-presence 'marked' context thickens it. |
| `--base-size-border-accent` | 4px (`--base-size-scale-4`) | — | Width of the shu marker under the current section. Decorative; never the only signal. |
| `--base-size-border-rule` | 4px (`--base-size-scale-4`) | — | Width of the heavy printed rule (masthead, fold, column tops). Hairlines use size.border.thin. |
| `--base-size-focus-ring-width` | 2px (`--base-size-scale-2`) | high-contrast | Focus ring thickness (WCAG 2.4.13 minimum 2px). Locked. |
| `--base-size-focus-ring-offset` | 2px (`--base-size-scale-2`) | — | Gap between the element and its focus ring. Locked. |
| `--base-size-container-sm` | 560px (`--base-size-scale-560`) | — | Dialogs, sign-in and single-column forms. |
| `--base-size-container-md` | 720px (`--base-size-scale-720`) | — | Reading width for settings and detail pages. |
| `--base-size-container-lg` | 1240px (`--base-size-scale-1240`) | — | Main content width of the app shell. |
| `--base-size-container-xl` | 1440px (`--base-size-scale-1440`) | — | Dashboards on very wide screens. |
| `--base-size-panel-nav` | 240px (`--base-size-scale-240`) | — | Side navigation width. |
| `--base-size-panel-drawer` | 480px (`--base-size-scale-480`) | — | Drawer width on wide screens; full width on narrow ones. |
| `--base-size-panel-popover` | 320px (`--base-size-scale-320`) | — | Maximum popover and tooltip width. |
| `--base-size-panel-menu` | 240px (`--base-size-scale-240`) | — | Minimum menu and listbox width. |
| `--base-size-breakpoint-sm` | 640px (`--base-size-scale-640`) | — | Narrow to medium layouts. CSS custom properties cannot be used in media queries; the build also emits these as constants. |
| `--base-size-breakpoint-md` | 960px (`--base-size-scale-960`) | — | Side navigation becomes visible. |
| `--base-size-breakpoint-lg` | 1240px (`--base-size-scale-1240`) | — | Wide layouts. |
| `--base-size-breakpoint-xl` | 1440px (`--base-size-scale-1440`) | — | Large desktop (MVDS 4.4). |
| `--base-size-seal` | 96px (`--base-size-scale-96`) | — | Width and height of a Seal: large enough for the mark and a short name. |
| `--base-size-rail` | 240px (`--base-size-scale-240`) | — | Width of the WarpRail. |
| `--base-size-thread` | 6px (`--base-size-scale-6`) | — | Width of a warp thread on the WarpRail. |
| `--base-size-cell` | 56px (`--base-size-scale-56`) | — | Minimum height of a cloth cell. |

Primitive tier (31):

- `--base-size-scale-*`: `1` 1px · `2` 2px · `3` 3px · `4` 4px · `6` 6px · `8` 8px · `12` 12px · `14` 14px · `16` 16px · `20` 20px · `24` 24px · `28` 28px · `32` 32px · `36` 36px · `40` 40px · `44` 44px · `48` 48px · `56` 56px · `64` 64px · `96` 96px · `240` 240px · `280` 280px · `320` 320px · `360` 360px · `480` 480px · `560` 560px · `640` 640px · `720` 720px · `960` 960px · `1240` 1240px · `1440` 1440px

Component tier: 385 tokens, listed per component under Components.

### Guidance

- **Spacing** guidance: not defined (foundation status: partial).
- Gap recorded for Spacing: Guidance per foundation lives in docs/tokens.md and the token descriptions; no foundation page yet.
- **Layout** guidance: not defined (foundation status: partial).
- Gap recorded for Layout: Guidance per foundation lives in docs/tokens.md and the token descriptions; no foundation page yet.
- **responsive** guidance: not defined (no responsive foundation in the records).

## Elevation & Depth

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-elevation-flat` | 0px 0px 0px 0px #00000000 (`--base-shadow-none`) | — | Content on the canvas. Base separates with rules, not shadows. |
| `--base-elevation-raised` | 0px 1px 2px 0px #13110f0f (`--base-shadow-1`) | — | A barely-there lift for cards that can be dragged or clicked. |
| `--base-elevation-overlay` | 0px 8px 24px 0px #13110f14 (`--base-shadow-2`) | dark | Menus, popovers, listboxes and toasts. |
| `--base-elevation-modal` | 0px 16px 48px 0px #13110f1f (`--base-shadow-3`) | dark | Dialogs and drawers. |

Primitive tier (6):

- `--base-shadow-*`: `1` 0px 1px 2px 0px #13110f0f · `2` 0px 8px 24px 0px #13110f14 · `3` 0px 16px 48px 0px #13110f1f · `none` 0px 0px 0px 0px #00000000
- `--base-shadow-dark-*`: `2` 0px 8px 24px 0px #00000080 · `3` 0px 16px 48px 0px #00000099

Component tier: 9 tokens, listed per component under Components.

### Stacking order

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-layer-content` | 0 (`--base-number-z-0`) | — | Normal flow. |
| `--base-layer-sticky` | 10 (`--base-number-z-10`) | — | Sticky headers and table heads. |
| `--base-layer-overlay` | 100 (`--base-number-z-100`) | — | Menus, popovers and listboxes. |
| `--base-layer-modal` | 200 (`--base-number-z-200`) | — | Modal dialogs and drawers with their backdrop. |
| `--base-layer-toast` | 300 (`--base-number-z-300`) | — | Toasts above modals so outcomes stay visible. |
| `--base-layer-tooltip` | 400 (`--base-number-z-400`) | — | Tooltips above everything. Focus is never hidden behind a layer (WCAG 2.4.11). |

Primitive tier (8):

- `--base-number-z-*`: `0` 0 · `1` 1 · `10` 10 · `100` 100 · `200` 200 · `300` 300 · `400` 400 · `500` 500

Component tier: 9 tokens, listed per component under Components.

### Guidance

- **Elevation** guidance: not defined (foundation status: partial).
- Gap recorded for Elevation: Guidance per foundation lives in docs/tokens.md and the token descriptions; no foundation page yet.

## Shapes

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-radius-control` | 2px (`--base-radius-scale-1`) | — | Corners of buttons, inputs and chips: 2 px, a cut edge of cloth. |
| `--base-radius-surface` | 2px (`--base-radius-scale-1`) | — | Cards, alerts, dialogs: square sheets. |
| `--base-radius-pill` | 9999px (`--base-radius-scale-full`) | — | Tags, badges, avatars and the switch. |
| `--base-radius-none` | 0px (`--base-radius-scale-0`) | — | Tables and full-bleed regions. |

Primitive tier (6):

- `--base-radius-scale-*`: `0` 0px · `1` 2px · `2` 4px · `3` 6px · `4` 10px · `full` 9999px

Component tier: 60 tokens, listed per component under Components.

### Guidance

- **shape** guidance: not defined (no shape foundation in the records).

## Motion

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-motion-duration-instant` | 0ms (`--base-duration-0`) | — | No animation. |
| `--base-motion-duration-fast` | 100ms (`--base-duration-100`) | reduced | Hover and press feedback. |
| `--base-motion-duration-moderate` | 160ms (`--base-duration-160`) | reduced | Small enter and exit: menus, tooltips. |
| `--base-motion-duration-slow` | 240ms (`--base-duration-240`) | reduced | Larger surfaces: dialogs, drawers. |
| `--base-motion-easing-standard` | cubic-bezier(0.2, 0, 0, 1) (`--base-easing-standard`) | — | Movement within the screen. |
| `--base-motion-easing-enter` | cubic-bezier(0, 0, 0, 1) (`--base-easing-enter`) | — | Things arriving. |
| `--base-motion-easing-exit` | cubic-bezier(0.3, 0, 1, 1) (`--base-easing-exit`) | — | Things leaving. |
| `--base-motion-easing-press` | cubic-bezier(0.3, 1.4, 0.6, 1) (`--base-easing-press`) | — | The seal landing: a short overshoot. Seal only; everything else uses standard easing. Reduced motion makes it instant. |

Primitive tier (10):

| Token | Value | Also set in | Role |
|---|---|---|---|
| `--base-easing-press` | cubic-bezier(0.3, 1.4, 0.6, 1) | — | A short overshoot, like a stamp pressed onto paper. Atelier Press uses it for the seal only. |

- `--base-duration-*`: `0` 0ms · `100` 100ms · `160` 160ms · `240` 240ms · `360` 360ms
- `--base-easing-*`: `standard` cubic-bezier(0.2, 0, 0, 1) · `enter` cubic-bezier(0, 0, 0, 1) · `exit` cubic-bezier(0.3, 0, 1, 1) · `linear` cubic-bezier(0, 0, 1, 1)

Component tier: 8 tokens, listed per component under Components.

### Guidance

- **Motion** guidance: not defined (foundation status: partial).
- Gap recorded for Motion: Guidance per foundation lives in docs/tokens.md and the token descriptions; no foundation page yet.

## Components

69 components from the component contracts. Use the exact import; full props and examples come from `get_entity`. 69 of them record open contract gaps (`get_entity` lists them).

### AILabel

Tells people, every time, that content was made or changed by AI, and lets them find out how.

`import { AILabel } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Next to any AI-generated or AI-edited text, summary, suggestion or image; On AI-drafted changes before review
- **Do not use when:** Explaining the whole AI feature at first use → The onboarding pattern with a Dialog or Card; Errors or low confidence → AIStatus
- **Variants:** not defined
- **States:** closed, open, subtle presence, marked presence (Provider aiPresence): Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: button; A real button named "AI" that opens a labelled non-modal dialog; the label is never only an icon; Native role: button; Pressable trigger and a modal explanation
- **Keyboard:** `Enter` Opens the explanation (Popover); `Escape` Closes it and returns focus
- **Do:** Every AI-generated content block carries an AILabel (EU AI Act Art. 50); AI colours (color.ai.*) are locked; brand layers cannot change them; Label "AI" (or a translated equivalent), never a mascot name; Explanation: what the AI did, what it used, what to check. Two sentences; Never claim accuracy ("verified", "guaranteed")
- **Tokens:** not defined

### AIStatus

Is honest about what the AI could not do, or might have got wrong, and says what to do next.

`import { AIStatus } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Caveats on generated content, low-confidence results, refusals, partial and full failures
- **Do not use when:** Non-AI errors → Alert; Marking content as AI-made → AILabel
- **Variants:** `kind`: caveat | low-confidence | refusal | partial-failure | error. caveat: a limit to keep in mind; low-confidence: check this; refusal: the AI will not or cannot do it; partial-failure and error: something went wrong
- **States:** caveat, low-confidence, refusal, partial-failure, error: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: status; role alert for error and partial failure, status otherwise. Never blames the user; Native role: status; The same roles on a View; Kind is in the title words and icon, not colour alone
- **Keyboard:** `Tab` Retry is reachable
- **Do:** Refusals and errors offer at least one next step or retry; Say what happened and what to do; never blame the user; Refusals explain the rule, not the model ("Locked tokens need the owner"); No anthropomorphic apologies beyond one short phrase
- **Tokens:** not defined

### Alert

Tells people about something that affects the page or their task, in place, where they will see it.

`import { Alert } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Results of an action that stay relevant (published, failed); Warnings about the current page (sync paused, read-only); Page-level errors that are not about one field
- **Do not use when:** Errors on specific fields after submit → ErrorSummary plus field errors; Brief confirmation that needs no action → ToastRegion; Decisions people must make → Dialog
- **Variants:** `tone`: info | success | warning | danger. The kind of message; also chooses the icon and the role (danger interrupts)
- **States:** info, success, warning, danger, dismissible: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: status; role alert (assertive) for danger, status (polite) otherwise. Insert the alert into the page to have it announced; do not toggle its visibility; Native role: status; The same roles on a View; Android announces live regions, iOS reads the content when focused; Tone is in the icon, the border and the words, never colour alone (1.4.1); Status messages are programmatically determinable (4.1.3)
- **Keyboard:** `Tab` Only the dismiss button and links inside are focusable
- **Do:** Do not make danger alerts dismissible when the problem still exists; Error: what happened, why, what to do next; Title is optional and short; the message stands alone; Never say "Error" or "Oops" as the whole title
- **Tokens:** 40 tokens (component tier, matched by name; inferred): `--base-alert-message-font-family`, `--base-alert-message-font-size`, `--base-alert-message-font-weight`, +37 more

### AppHeader

Anchors every page with the product name, the current context and global actions.

`import { AppHeader } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Top of the app shell on every page
- **Do not use when:** Page-specific titles and actions → PageHeader; Section navigation → SideNav
- **Variants:** not defined
- **States:** default: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: banner; header element at page level is the banner landmark; one per page; Native role: banner; View role banner
- **Keyboard:** `Tab` Moves focus (next)
- **Do:** One AppHeader per page; Product name as the wordmark text; context in the user's terms
- **Tokens:** 21 tokens (component tier, matched by name; inferred): `--base-app-header-brand-outline-color`, `--base-app-header-brand-outline-width`, `--base-app-header-brand-outline-offset`, +18 more

### AppShell

Gives every app page the same frame and landmarks, so people and screen readers always know where they are.

`import { AppShell } from "@loom/base/native";` · lifecycle beta · tier template

- **Use when:** Every signed-in page of the platform
- **Do not use when:** Sign-in, error and marketing pages → A centred Box or Card without navigation
- **Variants:** `frame`: stacked | rail. stacked: header on top, navigation beside main (SideNav or none). rail: the Selvedge frame, a full-height WarpRail on the left with the header and main beside it; under 48rem the rail becomes a sticky bottom bar after main
- **States:** with and without navigation, with notifications: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: main; Landmarks: banner (AppHeader), navigation (SideNav), main, region (ToastRegion); Native role: main; View with role main; the skip link renders nothing on native; 2.4.1 bypass blocks; 1.3.1 landmarks; reflow at 320 px stacks navigation above main (1.4.10)
- **Keyboard:** `Tab` Skip link, header, navigation, main content
- **Do:** Pages use AppShell so the skip link, banner, navigation and main landmarks exist; One h1 per page, inside main (PageHeader)
- **Tokens:** 12 tokens (component tier, matched by name; inferred): `--base-app-shell-background`, `--base-app-shell-color`, `--base-app-shell-main-padding-inline`, +9 more

### Avatar

Helps people recognise who did something or who owns something.

`import { Avatar } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Owners, reviewers and authors in lists, timelines and headers
- **Do not use when:** Product or system logos → An image with alt text; Status indicators → Badge
- **Variants:** `size`: sm | md | lg. 24, 32 or 48 px
- **States:** photo, initials, decorative: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: img; role img named by the person; the photo has empty alt so the name is not read twice; Native role: img; View role img with aria-label
- **Keyboard:** not defined
- **Do:** Avatar always has a name; Always pass the full name; initials are derived; Set decorative when the name is printed next to the avatar
- **Tokens:** 15 tokens (component tier, matched by name; inferred): `--base-avatar-sm-width`, `--base-avatar-sm-height`, `--base-avatar-md-width`, +12 more

### Badge

Marks status or a count on an item so people can scan a list quickly.

`import { Badge } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Lifecycle (Draft, Stable, Deprecated), counts of items needing attention
- **Do not use when:** A filter or a removable value → Tag; A message people must read → Alert; An action → Button
- **Variants:** `tone`: neutral | info | success | warning | danger | ai. Status colour. Pair with words that carry the meaning
- **States:** neutral, info, success, warning, danger, ai: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: text; Plain text in the reading order; label replaces a bare number with words; Native role: text; The visible Text carries label as its accessible name (React Native cannot position visually hidden text off-screen); Every tone pair meets 4.5:1 in all modes (contrast report)
- **Keyboard:** not defined
- **Do:** The text names the status; tone only reinforces it; One or two words, sentence case; Counts above 99 show "99+" with the exact number in label
- **Tokens:** 26 tokens (component tier, matched by name; inferred): `--base-badge-font-family`, `--base-badge-font-size`, `--base-badge-font-weight`, +23 more

### BandScale

Places a system on a named scale people already know (the Loom health bands: Tightly woven 90 and above, Holding together 75 to 89, Taking shape 60 to 74, Loose threads below 60), so its state reads as a sentence, not a score.

`import { BandScale } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Health bands, maturity levels, any ordered scale of three to five named steps
- **Do not use when:** Continuous values → ProgressBar or Numeral; Choosing a value → RadioGroup
- **Variants:** not defined
- **States:** default, current: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; An ordered list named by label; the current band has aria-current and visible words, so the position never depends on colour or pattern; Native role: list; The current band is selected and carries the same words
- **Keyboard:** not defined
- **Do:** Exactly one band is current; Order bands from lowest to highest; Band names are short phrases in the product's words; The current label names what sits there, for example the system
- **Tokens:** 31 tokens (component tier, matched by name; inferred): `--base-band-scale-gap`, `--base-band-scale-padding-block-start`, `--base-band-scale-border-block-start-color`, +28 more

### Box

Gives a region padding and a surface from the token roles, for layouts Card does not fit.

`import { Box } from "@loom/base/native";` · lifecycle beta · tier primitive

- **Use when:** Wells, panels and regions inside templates; Wrapping content in a sunken or surface background
- **Do not use when:** A titled group of content → Card; Spacing between siblings → Stack
- **Variants:** `padding`: none | sm | md | lg. Inset from the spacing scale; `background`: none | canvas | surface | sunken. Surface role; `border`: none | default | strong. Border role; `radius`: none | control | surface. Corner radius role
- **States:** padding, background, border and radius combinations: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; Native role: none
- **Keyboard:** not defined
- **Do:** No style overrides on Box; every option is a token role; No content of its own
- **Tokens:** 13 tokens (component tier, matched by name; inferred): `--base-box-sm-padding`, `--base-box-md-padding`, `--base-box-lg-padding`, +10 more

### Breadcrumbs

Shows people where they are and lets them go up a level.

`import { Breadcrumbs } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Pages more than two levels deep in a hierarchy (system, component, token)
- **Do not use when:** Flat sites or the top level → Nothing; the page title is enough; Steps in a process → Wizard template steps
- **Variants:** not defined
- **States:** link, current page: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: navigation; nav landmark with an ordered list; aria-current="page" on the last item; separators are decorative; Native role: navigation; The current item is exposed as selected; links open with the platform
- **Keyboard:** `Tab` Each link is a tab stop; the current page is not
- **Do:** Exactly one item is current and it is the last; Use the page titles, shortened if needed; The last item is the current page and matches its h1
- **Tokens:** 16 tokens (component tier, matched by name; inferred): `--base-breadcrumbs-link-outline-color`, `--base-breadcrumbs-link-outline-width`, `--base-breadcrumbs-link-outline-offset`, +13 more

### Button

Starts an action in the current view: save, publish, open a dialog, run a check.

`import { Button } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** To start an action that changes data or opens something on this page; To submit a form (set it as the form's last control); As a toggle (pressed) for an on/off formatting or view option in a toolbar
- **Do not use when:** Going to another page or URL → Link; Choosing one of several options → RadioGroup or Select; Turning a setting on or off that takes effect immediately → Switch; A list of more than three related actions → Menu
- **Variants:** `tone`: primary | secondary | quiet | danger. primary: the one main action in a view; secondary: other actions; quiet: toolbars and dense areas; danger: removes or loses something; `size`: md | sm. md for forms and pages; sm for toolbars, tables and cards. Both keep a 24px minimum target
- **States:** default, hover, focus-visible, pressed, selected (toggle): Supported (contract docs.states); busy: Exposed as busy when busy; disabled: Exposed as disabled when disabled \|\| busy; error: Not supported: A button has no error state; show the error next to what failed; read-only: Not supported: Not applicable to actions
- **Accessibility:** Target: WCAG 2.2 AA; Web role: button; Native <button type="button"> so it never submits a form by accident; Toggle form uses aria-pressed; the label must not change when pressed; Native role: button; TalkBack and VoiceOver activate through the activate accessibility action; Toggle form exposes the selected state (native has no aria-pressed); Minimum target 24×24 CSS px (2.5.8) through size.target.min; Busy keeps focus so keyboard users do not lose their place (2.4.3)
- **Keyboard:** `Enter` PRESS; `Space` PRESS; `Tab` A disabled button is skipped; a busy one is not
- **Do:** At most one tone="primary" Button per view or dialog; Do not navigate with Button; use Link for navigation; iconOnly buttons keep a descriptive children label; Start with a verb and name the object: "Publish release"; Sentence case, no end punctuation, at most 4 words; One primary button per view or region; Never "Click here"; never change the label when busy (busy is announced)
- **Tokens:** 46 tokens (component tier, matched by name; inferred): `--base-button-outline-color`, `--base-button-outline-width`, `--base-button-outline-offset`, +43 more

### ButtonGroup

Keeps related actions together with the right spacing and order.

`import { ButtonGroup } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Form and dialog footers; Two or three related actions in a toolbar
- **Do not use when:** Many actions → Menu for the less used ones; Choosing one option → RadioGroup (segmented control is an expansion item)
- **Variants:** `align`: start | end | between. Horizontal alignment; end for dialog footers; `orientation`: horizontal | vertical. vertical stacks full-width buttons (narrow layouts)
- **States:** horizontal, vertical: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: group; role group with aria-label; Native role: group; View role group; its buttons stay individually reachable
- **Keyboard:** `Tab` Each button is its own tab stop
- **Do:** At most one primary Button in a group; Safe action first in reading order, primary or danger last; At most one primary
- **Tokens:** 1 token (component tier, matched by name; inferred): `--base-button-group-gap`

### Card

Groups related content and actions about one subject on a quiet surface.

`import { Card } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Dashboard tiles, summaries, settings groups, items in a grid
- **Do not use when:** Page sections that do not need a frame → Heading and Stack; Messages about state → Alert; Tabular data → Table
- **Variants:** `variant`: outlined | raised | sunken. outlined by default; raised only for draggable or floating cards; sunken for secondary groups
- **States:** outlined, raised, sunken: Supported (contract docs.states); selected: Not supported: Selectable cards are not in v1
- **Accessibility:** Target: WCAG 2.2 AA; Web role: group; A div with a real heading, so the outline includes it; Native role: none; View with a heading Text
- **Keyboard:** not defined
- **Do:** Do not nest Cards; use sections or Disclosure inside; Title is a noun phrase; description says why it matters; At most two actions in the footer
- **Tokens:** 27 tokens (component tier, matched by name; inferred): `--base-card-body-font-family`, `--base-card-body-font-size`, `--base-card-body-font-weight`, +24 more

### Checkbox

Lets people make a yes/no choice that takes effect when the form is submitted.

`import { Checkbox } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Consent or confirmation statements; An option inside a form that is saved with the form; "Select all" with a mixed state over a list
- **Do not use when:** The setting applies immediately → Switch; Several related options in one question → CheckboxGroup; Only one of several options may be chosen → RadioGroup
- **Variants:** not defined
- **States:** unchecked, mixed, hover, focus-visible, invalid: Supported (contract docs.states); checked: Exposed as checked when state(on); disabled: Exposed as disabled when disabled; loading: Not supported: Checkboxes do not save on their own
- **Accessibility:** Target: WCAG 2.2 AA; Web role: checkbox; A native checkbox input inside a label; indeterminate uses the input's indeterminate property; Native role: checkbox; Mixed state is aria-checked="mixed"; The whole label is the click target (2.5.8)
- **Keyboard:** `Space` TOGGLE; `Tab` Moves focus (next)
- **Do:** Two or more checkboxes answering one question go in a CheckboxGroup with a label; Write the label as a positive statement people can agree with; Sentence case; no end punctuation unless the label is a full sentence; Explain consequences in the hint, not the label
- **Tokens:** 28 tokens (component tier, matched by name; inferred): `--base-checkbox-box-outline-color`, `--base-checkbox-box-outline-width`, `--base-checkbox-box-outline-offset`, +25 more

### CheckboxGroup

Asks one question that can have several answers.

`import { CheckboxGroup } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Filters and preferences with several independent options; Any multi-answer question in a form
- **Do not use when:** Only one answer is allowed → RadioGroup; A single yes/no statement → Checkbox; Many options (more than about 10) → ComboBox with multiple selection (expansion item, not in v1)
- **Variants:** not defined
- **States:** unchecked, checked, hover, focus-visible, invalid, disabled: Supported (contract docs.states); mixed: Not supported: Use a separate Checkbox with indeterminate for select-all
- **Accessibility:** Target: WCAG 2.2 AA; Web role: group; Labelled group of native checkboxes; hint and error describe the group; Native role: group; Container labelled by its label; each option is a checkbox with aria-checked
- **Keyboard:** `Tab` Each checkbox is its own tab stop; `Space` TOGGLE
- **Do:** Every CheckboxGroup has a label; Label the group with the question; Options are parallel and independent
- **Tokens:** 39 tokens (component tier, matched by name; inferred): `--base-checkbox-group-label-font-family`, `--base-checkbox-group-label-font-size`, `--base-checkbox-group-label-font-weight`, +36 more

### Citation

Lets people check where a generated claim came from, right where they read it.

`import { Citation } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** After each generated sentence or claim that relies on a source
- **Do not use when:** Listing all sources → SourcesList; Ordinary links in human-written text → Link
- **Variants:** not defined
- **States:** default, focus-visible: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: link; Visible number, accessible name "Source 2: title" so it makes sense out of context; Native role: link; Text link with the same accessible name; 2.4.4 link purpose via the accessible name
- **Keyboard:** `Tab` Moves focus (next); `Enter` Follows the link
- **Do:** Every Citation index exists in the SourcesList on the same answer; Numbers match the SourcesList order; Titles are the source's own title
- **Tokens:** 10 tokens (component tier, matched by name; inferred): `--base-citation-link-outline-color`, `--base-citation-link-outline-width`, `--base-citation-link-outline-offset`, +7 more

### Cloth

Shows a whole design system as one fabric: every component family in every product, its health drawn as colour, weave density and words, with the open problems pulled out to the edge as loose threads.

`import { Cloth } from "@loom/base/native";` · lifecycle beta · tier pattern

- **Use when:** The system overview: health of families × products; Any two-axis health or coverage view where each cell has a named band
- **Do not use when:** One value over time → A chart, or ColourBlock for a single figure; A list of records to sort and filter → Table; More than about 8 columns → Table, or split the cloth by product group
- **Variants:** `view`: cloth | table. cloth draws each cell as woven cloth; table shows the same table plainly (text only), for printing, dense views or people who prefer it
- **States:** default, linked cell, table view: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: table; table, caption, th scope col and row; each cell's text is its full accessible name; Native role: table; View with table, row and cell roles; each cell is one accessible element named by cell.name; A real table with a caption, column headers (scope col) and row headers (scope row), so screen readers announce family and product for every cell; Each cell's text is its full name (band and count); the visible chip is hidden from assistive technology so nothing is read twice; Cells with open threads hold a link that covers the cell and is a tab stop; the other cells are reached with table navigation; Weave patterns differ in shape (plain weave, small gaps, open lattice, broken slanted threads) and each cell shows a glyph and a word: colour is never the only signal (WCAG 1.4.1); The scroll region is a named, focusable region so the table can be scrolled with a keyboard on narrow screens
- **Keyboard:** `Tab` Moves through the scroll region, then each linked cell
- **Do:** Every cell has an accessible name with family, product, band and count; Health is never shown by colour alone: each band has its own weave shape, glyph and words; Each row has one cell per product, in column order; Every cell carries a band and a full accessible name: family, product, band, count; Keep visible labels to one short word; the full band name goes in the name; Link a cell when it has open loose threads, to the first of them; Bands: woven (Tightly woven), holding (Holding together), shape (Taking shape), loose (Loose threads)
- **Tokens:** 64 tokens (component tier, matched by name; inferred): `--base-cloth-gap`, `--base-cloth-scroller-border-radius`, `--base-cloth-table-border-spacing`, +61 more

### ColourBlock

Gives the two or three numbers that matter this week the weight of a dye field, so a head of design reads them before anything else.

`import { ColourBlock } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Two to four key figures under the cloth: drift, drafts to decide, coverage; A single figure that needs one action
- **Do not use when:** Many small figures → A table or a definition list; A trend → A chart; Status of one record → Badge or Alert
- **Variants:** `tone`: indigo | madder | saffron | teal. Dye: indigo (See), madder (Shape), saffron (Govern), teal (Run). Saffron is a light field with ink text
- **States:** indigo, madder, saffron, teal, with action: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; A heading, the figure as text, one link. The colour carries no information of its own; Native role: none; View with Text and a Pressable link; Text on each dye passes AA: cotton on indigo 11.0:1, on madder 5.7:1, on teal 5.5:1, ink on saffron 8.4:1 (contrast gate, every mode); Inside the field the focus ring is cotton (ink on saffron), not indigo, so it stays visible on the dye
- **Keyboard:** `Tab` Moves to the link
- **Do:** Tone follows the thread the figure belongs to; never use madder only to alarm; Label: one or two words; Value: a formatted figure, never a sentence; Description: one sentence with the change since last time; One action at most
- **Tokens:** 49 tokens (component tier, matched by name; inferred): `--base-colour-block-link-outline-color`, `--base-colour-block-link-outline-width`, `--base-colour-block-link-outline-offset`, +46 more

### ComboBox

Lets people find and choose one option from a long list by typing.

`import { ComboBox } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** More than about 15 options (people, repositories, countries); People know what they are looking for
- **Do not use when:** A short list where browsing is easier → Select; Free text with suggestions only (search) → TextField type="search"
- **Variants:** not defined
- **States:** closed, open, filtered, no matches, hover, focus, invalid, disabled: Supported (contract docs.states); multiple: Not supported: Multi-select tags input is an expansion item
- **Accessibility:** Target: WCAG 2.2 AA; Web role: combobox; Input role combobox with aria-autocomplete=list and aria-controls the listbox; Native role: combobox; TextInput with role combobox and aria-expanded; options render inline below as role option
- **Keyboard:** `ArrowDown` Opens the list or moves to the next option (the input keeps focus; aria-activedescendant moves); `ArrowUp` Moves focus (previous); `Enter` SELECT when state(open); `Escape` Closes the list; a second Escape clears the text
- **Do:** Prefer ComboBox over Select above 15 options; Option labels start with the word people type; The no-matches text says what to do: "No matches. Check the spelling."
- **Tokens:** 63 tokens (component tier, matched by name; inferred): `--base-combo-box-gap`, `--base-combo-box-label-font-family`, `--base-combo-box-label-font-size`, +60 more

### ConfirmAction

Puts a person in control before an AI agent changes anything: it shows the plan, asks, reports the outcome and offers undo.

`import { ConfirmAction } from "@loom/base/native";` · lifecycle beta · tier pattern

- **Use when:** Any AI action that writes user data or is hard to reverse (required); Agent drafts in Suggest mode
- **Do not use when:** Read-only AI answers → StreamingText; Human-initiated destructive actions → Dialog variant="alert"
- **Variants:** `status`: pending | running | done | cancelled. pending: waiting for a person; running; done (undo available); cancelled
- **States:** pending, running, done (undo), cancelled: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: region; A region labelled by its title; outcome is a polite status; Native role: region; View role region with aria-label; outcome Text is a polite live region; 3.3.4 error prevention: reversible, checked or confirmed
- **Keyboard:** `Tab` Cancel, then Confirm
- **Do:** AI actions that write data go through ConfirmAction unless the area is in Full autonomy; Offer undo whenever the change can be reverted; Title: verb, count, object: "Rename 3 tokens"; Description: what changes, where, and whether it can be undone; Confirm label repeats the action
- **Tokens:** 29 tokens (component tier, matched by name; inferred): `--base-confirm-action-description-font-family`, `--base-confirm-action-description-font-size`, `--base-confirm-action-description-font-weight`, +26 more

### Dialog

Asks for a decision or a short input that must happen before people continue, blocking the rest of the page.

`import { Dialog } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Confirming a destructive or irreversible action (variant="alert"); A short focused task: rename, invite, set a value; Confirm-before-act for AI actions that write data
- **Do not use when:** Information that does not need a decision → Alert or Toast; Long forms or browsing → A page, or Drawer for related detail; Small anchored detail → Popover
- **Variants:** `variant`: default | alert. alert: a confirmation that interrupts (role alertdialog); `size`: sm | md. sm for confirmations and short forms; md for longer forms
- **States:** closed, open, not dismissible (saving), alert: Supported (contract docs.states); nested: Not supported: Never open a dialog from a dialog
- **Accessibility:** Target: WCAG 2.2 AA; Web role: dialog; variant alert uses role alertdialog; Native role: dialog; aria-modal maps to accessibilityViewIsModal on iOS; The Android back button reaches CLOSE through Modal onRequestClose; Focus trapped while open and returned to the opener (2.4.3); Background content is hidden from assistive technology while open
- **Keyboard:** `Escape` CLOSE; `Tab` Tab and Shift+Tab stay inside the dialog
- **Do:** Destructive confirmations use variant="alert" and a danger Button; Do not open a Dialog from inside a Dialog; Title is the question or the task: "Delete the Atlas project?"; Description names the consequence and the object; Buttons say the action ("Delete project", "Keep project"), never "Yes"/"No"; Put the safe action first in reading order; the destructive one is tone="danger"
- **Tokens:** 42 tokens (component tier, matched by name; inferred): `--base-dialog-close-outline-color`, `--base-dialog-close-outline-width`, `--base-dialog-close-outline-offset`, +39 more

### Disclosure

Hides optional detail until people ask for it, keeping pages short and calm.

`import { Disclosure } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** FAQs, advanced settings, details most people skip; An accordion: a Stack of Disclosures
- **Do not use when:** Content everyone needs → Show it; do not hide required information; Switching between peer views → Tabs; Temporary information on hover or focus → Tooltip
- **Variants:** not defined
- **States:** collapsed, hover, focus-visible, disabled: Supported (contract docs.states); expanded: Exposed as expanded when state(expanded)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: button; Trigger button inside a heading, aria-expanded and aria-controls the panel; the hidden panel is removed from the accessibility tree; Native role: button; Pressable with aria-expanded; react-stately useDisclosureState shared with web; The chevron rotation honours reduced motion
- **Keyboard:** `Enter` TOGGLE; `Space` TOGGLE; `Tab` Moves focus (next)
- **Do:** headingLevel follows the page outline; Titles are specific questions or nouns; Never hide errors or required inputs inside a collapsed disclosure
- **Tokens:** 27 tokens (component tier, matched by name; inferred): `--base-disclosure-trigger-outline-color`, `--base-disclosure-trigger-outline-width`, `--base-disclosure-trigger-outline-offset`, +24 more

### Divider

Separates groups of content with a thin rule when spacing alone is not enough.

`import { Divider } from "@loom/base/native";` · lifecycle beta · tier primitive

- **Use when:** Between groups in menus, toolbars, settings and side navigation
- **Do not use when:** Separating every item in a list → Spacing (Stack gap) or row borders in Table; Framing a group → Card
- **Variants:** `orientation`: horizontal | vertical. Direction of the rule
- **States:** horizontal, vertical, decorative: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: separator; role separator with aria-orientation; decorative dividers are aria-hidden; Native role: separator; View role separator; The rule is decorative contrast (border.default); it never carries meaning on its own
- **Keyboard:** not defined
- **Do:** Prefer spacing; use dividers only where groups would otherwise blur; No text inside a divider
- **Tokens:** 1 token (component tier, matched by name; inferred): `--base-divider-background`

### Drawer

Shows related detail or a longer task beside the page, so people keep their place in the list behind it.

`import { Drawer } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Detail of a list row (list to detail on narrow layouts); Filters on small screens; Editing an item without leaving the list
- **Do not use when:** A quick decision → Dialog; Primary navigation → SideNav; Content people need side by side for long periods → A two-column page layout (Grid)
- **Variants:** `placement`: end | start. Side it slides from (logical: end is right in left-to-right)
- **States:** closed, open, not dismissible: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: dialog; Native role: dialog; Rendered full screen in a Modal on native
- **Keyboard:** `Escape` CLOSE; `Tab` Moves focus (wrap-within)
- **Do:** One drawer at a time; never stack drawers; Title names the object or the task; Actions at the end of the content, in a ButtonGroup
- **Tokens:** 37 tokens (component tier, matched by name; inferred): `--base-drawer-close-outline-color`, `--base-drawer-close-outline-width`, `--base-drawer-close-outline-offset`, +34 more

### EmptyState

Turns an empty view into guidance: why it is empty and what to do.

`import { EmptyState } from "@loom/base/native";` · lifecycle beta · tier pattern

- **Use when:** First use, no search results, cleared lists, no permission
- **Do not use when:** Errors → Alert or the error page template; Loading → Skeleton
- **Variants:** not defined
- **States:** first use, no results, cleared, no permission: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; A heading and text in reading order; the icon is decorative; Native role: none; The same structure in Views
- **Keyboard:** `Tab` Moves focus (next)
- **Do:** actionLabel starts with a verb; Explain, then act; No blame: "No matches", not "You entered nothing valid"; One primary action at most
- **Tokens:** 15 tokens (component tier, matched by name; inferred): `--base-empty-state-gap`, `--base-empty-state-padding-block`, `--base-empty-state-padding-inline`, +12 more

### ErrorSummary

After a failed submit, tells people everything that is wrong in one place and takes them to each field.

`import { ErrorSummary } from "@loom/base/native";` · lifecycle beta · tier pattern

- **Use when:** Forms and wizard steps with more than one field, after submit fails validation
- **Do not use when:** One field, validated as people type → The field's error only; Errors not tied to fields → Alert tone="danger"
- **Variants:** not defined
- **States:** shown: Supported (contract docs.states); dismissible: Not supported: It goes away when the errors are fixed and the form is resubmitted
- **Accessibility:** Target: WCAG 2.2 AA; Web role: alert; role alert, tabindex -1 and focused on mount; links point at field ids (the field inputs must have those ids); Native role: alert; View role alert; errors are text (in-page links do not exist on native; the screen reader moves to the fields); 3.3.1 error identification, 3.3.3 error suggestion, 2.4.3 focus order
- **Keyboard:** `Tab` From the summary to each error link; `Enter` A link moves focus to its field
- **Do:** Every error id matches an input id on the page; Messages match the field errors exactly; Say what to do: "Enter a project name"; In form order
- **Tokens:** 23 tokens (component tier, matched by name; inferred): `--base-error-summary-outline-color`, `--base-error-summary-outline-width`, `--base-error-summary-outline-offset`, +20 more

### FeedbackControls

Lets people tell the system whether an AI response helped, ask for another, or report a problem.

`import { FeedbackControls } from "@loom/base/native";` · lifecycle beta · tier pattern

- **Use when:** Under every AI response
- **Do not use when:** Feedback about the product in general → A feedback form
- **Variants:** `rating`: none | up | down. Current rating; you store it
- **States:** unrated, rated up, rated down: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: group; Rating buttons are toggles (aria-pressed); icon-only buttons are named by their labels; the thank-you is a polite status; Native role: group; Ratings expose the selected state (native has no aria-pressed)
- **Keyboard:** `Tab` Each control is a tab stop; `Space` Toggles a rating
- **Do:** Say what feedback is stored and who sees it in the report Dialog; Labels are short and neutral: "Helpful", "Not helpful"; Thank once, politely; never ask why in a popup on every rating; Report opens a Dialog with what happens to the report
- **Tokens:** 5 tokens (component tier, matched by name; inferred): `--base-feedback-controls-thanks-font-family`, `--base-feedback-controls-thanks-font-size`, `--base-feedback-controls-thanks-line-height`, +2 more

### FormField

Connects a label, hint and error to a control that does not already include them.

`import { FormField } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Wrapping a custom or third-party input (a colour picker, a code editor) so it is labelled and described
- **Do not use when:** Text, choices and selects from Base → TextField, TextArea, Select, ComboBox, RadioGroup, CheckboxGroup (they include the field); Grouping several controls under one question → RadioGroup or CheckboxGroup
- **Variants:** not defined
- **States:** default, invalid: Supported (contract docs.states); disabled: Not supported: The control owns its disabled state
- **Accessibility:** Target: WCAG 2.2 AA; Web role: group; Uses react-aria useField: the label's id names the control, hint and error ids describe it; Native role: none; The control receives aria-label and accessibilityHint because native has no id references (labelledby is Android-only)
- **Keyboard:** not defined
- **Do:** FormField wraps exactly one control; Same rules as TextField: visible noun-phrase label, actionable error
- **Tokens:** 16 tokens (component tier, matched by name; inferred): `--base-form-field-gap`, `--base-form-field-label-font-family`, `--base-form-field-label-font-size`, +13 more

### Grid

Arranges cards and tiles in equal columns that adapt to the space available.

`import { Grid } from "@loom/base/native";` · lifecycle beta · tier primitive

- **Use when:** Dashboards, galleries of components, settings overviews
- **Do not use when:** One-dimensional lists → Stack; Tabular data → Table
- **Variants:** `columns`: 2 | 3 | 4 | auto. Target columns on wide screens; fewer appear when space is short. auto fits as many 240 px columns as there is room for; `gap`: sm | md | lg. Space between cells
- **States:** 2, 3, 4 or auto columns: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; Visual order equals source order; Native role: none; Native has no CSS grid: cells stack in one column; Collapses to one column at 320 px (1.4.10)
- **Keyboard:** not defined
- **Do:** Do not give Grid cells fixed widths; Cells in reading order; never rely on visual position for meaning
- **Tokens:** 3 tokens (component tier, matched by name; inferred): `--base-grid-sm-gap`, `--base-grid-md-gap`, `--base-grid-lg-gap`

### Heading

Names a section so people and assistive technology can scan the page structure.

`import { Heading } from "@loom/base/native";` · lifecycle beta · tier primitive

- **Use when:** Page titles (level 1), section and card titles
- **Do not use when:** Making text bigger or bolder for emphasis → Text with variant body-lg, or strong inside Text; Labels of form controls → The control's label prop
- **Variants:** `size`: display | headline-lg | headline-md | headline-sm | title | subtitle. Visual size from the type scale, independent of level
- **States:** default: Supported (contract docs.states); interactive: Not supported: Headings are not interactive; wrap a Link inside if needed
- **Accessibility:** Target: WCAG 2.2 AA; Web role: heading; A real h1 to h6 from level, so the document outline is right; Native role: heading; Text with role heading; native has no heading levels; Sizes use rem-based tokens so they scale with browser text settings (1.4.4)
- **Keyboard:** not defined
- **Do:** One level-1 Heading per page; Do not skip levels (h2 to h4); Sentence case, no end punctuation; Front-load the meaning: "Releases this month"; Japanese headings use word-break: auto-phrase so lines break at phrase boundaries
- **Tokens:** 19 tokens (component tier, matched by name; inferred): `--base-heading-font-family`, `--base-heading-title-font-family`, `--base-heading-subtitle-font-family`, +16 more

### HealthBand

Opens a page with its verdict in the system's own words, so the first thing anyone reads is how the cloth is holding up and what to do next.

`import { HealthBand } from "@loom/base/native";` · lifecycle beta · tier pattern

- **Use when:** Top of the overview and of each thread's main page; A page that has one clear verdict or question ("Ask the cloth.")
- **Do not use when:** Forms, settings and lists → PageHeader; Inside a page section → Heading
- **Variants:** `tone`: indigo | madder | saffron | teal. Dye of the field, by the page's thread: indigo (See), madder (Shape), saffron (Govern), teal (Run)
- **States:** indigo, madder, saffron, teal, with aside: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; A div holding the page's h1; actions are ordinary controls; Native role: none; View with Text; The headline is the page's h1; the band is in words, never only in colour; Controls inside use the field surface: cotton primary buttons and a cotton focus ring on dark dyes, ink on saffron
- **Keyboard:** not defined
- **Do:** One HealthBand per page, holding the h1; Headline: the band name or a short claim, with a full stop. Two to four words; Lede: the numbers behind it, one or two sentences; Eyebrow: thread · page · system; At most two actions
- **Tokens:** 40 tokens (component tier, matched by name; inferred): `--base-health-band-padding-inline`, `--base-health-band-padding-block-start`, `--base-health-band-padding-block-end`, +37 more

### Icon

Adds a recognisable symbol to support text, or stands in for text when it has a label.

`import { Icon } from "@loom/base/native";` · lifecycle beta · tier primitive

- **Use when:** Next to text to aid scanning; Status markers with a label
- **Do not use when:** An icon that does something when pressed → Button with icon and iconOnly; Illustration or brand marks → The illustration library (not part of Base v1)
- **Variants:** `size`: sm | md | lg. 16, 20 or 24 px; `tone`: inherit | subtle | info | success | warning | danger | ai. Colour role; inherit takes the surrounding text colour
- **States:** decorative, informative: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: img; Decorative icons are aria-hidden; labelled icons are role img with aria-label; Native role: img; Decorative icons are hidden from VoiceOver and TalkBack; Informative icons need 3:1 against their background (1.4.11); tone colours meet it
- **Keyboard:** not defined
- **Do:** name is an icon in the Base set; Labels describe meaning, not the drawing: "Needs review", not "Triangle"
- **Tokens:** 21 tokens (component tier, matched by name; inferred): `--base-ai-status-icon-width`, `--base-ai-status-icon-height`, `--base-ai-status-icon-color`, +18 more

### Link

Takes people somewhere else: a page, a section of this page or another site.

`import { Link } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Navigation between pages or to a section; Opening documentation or an external site; Inline references inside a sentence
- **Do not use when:** Doing something on this page (save, delete, open a dialog) → Button; Primary page navigation in the app shell → SideNav; Showing where the page sits in a hierarchy → Breadcrumbs
- **Variants:** `variant`: inline | standalone. inline: inside running text; standalone: on its own line or in a list, slightly heavier
- **States:** default, hover, focus-visible, visited (browser default): Supported (contract docs.states); disabled: Not supported: A link that goes nowhere should not render as a link; loading: Not supported: Navigation feedback belongs to the destination page
- **Accessibility:** Target: WCAG 2.2 AA; Web role: link; A real <a href> so middle-click, copy link and the browser's link list work; External links set target=_blank and rel=noopener noreferrer and announce it; Native role: link; Text with role link; the external announcement becomes the accessibility hint; Underlined by default so links do not rely on colour alone (1.4.1); Link purpose from the text itself (2.4.4)
- **Keyboard:** `Enter` FOLLOW; `Tab` Moves focus (next)
- **Do:** Link text is not "click here", "here", "more" or a bare URL; Do not use Link with an onPress that performs an action and no navigation; use Button; Describe the destination; never "click here" or "read more" alone; Keep inline links short: the words that name the destination; Say when a link opens a new tab, downloads a file or leaves the product
- **Tokens:** 12 tokens (component tier, matched by name; inferred): `--base-link-outline-color`, `--base-link-outline-width`, `--base-link-outline-offset`, +9 more

### LooseThreads

Turns open problems into something you can pick up: each loose thread says where the cloth frays, what is wrong and how bad it is, and links both to the fix and back to its cell.

`import { LooseThreads } from "@loom/base/native";` · lifecycle beta · tier pattern

- **Use when:** Beside a Cloth, as its threads slot; Any short list of open problems tied to a place in the system
- **Do not use when:** Long, filterable work queues → Table; One problem that blocks the page → Alert
- **Variants:** not defined
- **States:** default, with link back to the cell: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; A heading and an ordered list; each item's id is the anchor a Cloth cell links to; The knot glyph is decorative; the band is written beside it; Native role: list; View list with Pressable links
- **Keyboard:** `Tab` Moves through the thread links
- **Do:** Every thread names where it frays and its band in words; Where: family · product; Title: what is wrong, in plain words, as the link; Band in words beside the knot; the glyph is never the only signal; Most urgent first; keep it to about seven
- **Tokens:** 46 tokens (component tier, matched by name; inferred): `--base-loose-threads-link-outline-color`, `--base-loose-threads-link-outline-width`, `--base-loose-threads-link-outline-offset`, +43 more

### Masthead

Gives every page of a system the presence of a publication: who it belongs to, which issue you are reading and where the sections are.

`import { Masthead } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Top of the Atelier Press shell on every page; Any product that presents its state as a regular report: a weekly issue, a release, a sprint
- **Do not use when:** Dense tools where vertical space is scarce → AppHeader; The page title → PageHeader or StoryHeader
- **Variants:** not defined
- **States:** default, with accent, with issue line: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: banner; header element (banner landmark). The title is a paragraph so the page's h1 stays in the content; Title and accent are read as one phrase; Native role: none; View with Text; the title is not a heading on native either
- **Keyboard:** not defined
- **Do:** One Masthead (or AppHeader) per page; The title is the system or publication name, not a page name; Keep the accent to one word; The note is one short phrase, for example the health band; never a sentence
- **Tokens:** 38 tokens (component tier, matched by name; inferred): `--base-masthead-background`, `--base-masthead-padding-inline`, `--base-masthead-padding-block-start`, +35 more

### Menu

Groups secondary actions behind one button so the interface stays calm.

`import { Menu } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** More than two or three secondary actions on an object (row, card, page); Overflow ("More actions") in toolbars
- **Do not use when:** Choosing a value for a form → Select; The main action of a view → Button tone="primary"; Navigation between pages → SideNav or Link
- **Variants:** `tone`: secondary | quiet. Trigger emphasis
- **States:** closed, open, item hover, item focus, item disabled, danger item: Supported (contract docs.states); selection: Not supported: Menus run actions; checked items are not in v1
- **Accessibility:** Target: WCAG 2.2 AA; Web role: button; Trigger has aria-haspopup=menu; items are menuitem with roving focus and typeahead; Native role: button; Items render in a modal as role menuitem; Android back closes it; Danger items are marked by text and position, not colour alone
- **Keyboard:** `Enter` Opens and focuses the first item; `ArrowDown` OPEN when state(closed); `ArrowDown` Moves focus (next); `ArrowUp` Moves focus (previous); `Enter` ACTION when state(open); `Escape` CLOSE
- **Do:** A danger item opens a Dialog variant="alert" before acting; Items start with a verb: "Rename", "Export as CSV"; Destructive items last, marked danger, and they open a confirmation; At most about 7 items; group with a divider when longer (not in v1)
- **Tokens:** 41 tokens (component tier, matched by name; inferred): `--base-menu-outline-color`, `--base-menu-outline-width`, `--base-menu-outline-offset`, +38 more

### Numeral

Lets one number carry a page, so the most important measure is read first and nothing competes with it.

`import { Numeral } from "@loom/base/native";` · lifecycle beta · tier primitive

- **Use when:** The one figure that matters most on an overview or report
- **Do not use when:** Several figures of equal weight → Table or Text variant figure; Progress towards a target → ProgressBar
- **Variants:** not defined
- **States:** default: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; Label, figure and caption are read in order, so the figure is announced with what it measures; The figure is never the only carrier of meaning: the caption says what it means; Native role: none; Texts in a View, read in order
- **Keyboard:** not defined
- **Do:** At most one Numeral per page; One Numeral per page; The caption gives the change and its period, with numbers; Format the value for the locale
- **Tokens:** 22 tokens (component tier, matched by name; inferred): `--base-numeral-gap`, `--base-numeral-label-color`, `--base-numeral-label-font-family`, +19 more

### PageHeader

Opens each page with its title, context and the actions that apply to the whole page.

`import { PageHeader } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Top of list, detail, settings and dashboard pages
- **Do not use when:** Product-wide header → AppHeader; Section titles → Heading
- **Variants:** not defined
- **States:** with or without eyebrow, description, actions, breadcrumbs: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; A div, not a header element, so it is not a second banner landmark; Native role: none; A View with a heading Text
- **Keyboard:** not defined
- **Do:** One PageHeader with headingLevel 1 per page; Title matches the navigation label that led here; Eyebrow is uppercase Latin only; omit it for Japanese pages
- **Tokens:** 20 tokens (component tier, matched by name; inferred): `--base-page-header-gap`, `--base-page-header-padding-block`, `--base-page-header-row-gap`, +17 more

### Pagination

Lets people move through a long list one page at a time and know where they are.

`import { Pagination } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Lists and tables with more items than fit comfortably (more than about 50); When people need to return to a specific page
- **Do not use when:** Feeds people browse continuously → A "Load more" Button; Steps of a process → Wizard template
- **Variants:** not defined
- **States:** current page, hover, focus-visible, previous disabled, next disabled: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: navigation; The current page button has aria-current="page"; page buttons are named "Page 3"; Native role: navigation; The current page is exposed as selected (native has no aria-current)
- **Keyboard:** `Tab` Moves focus (next); `Enter` GO
- **Do:** With two paginations on a page, give each a distinct label; Keep the default labels unless the product calls items something specific
- **Tokens:** 40 tokens (component tier, matched by name; inferred): `--base-pagination-page-font-family`, `--base-pagination-page-font-size`, `--base-pagination-page-font-weight`, +37 more

### Popover

Shows a little extra detail or a few controls next to the thing they relate to, without leaving the page or blocking it.

`import { Popover } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Explaining a term or a status with a link to more; A small set of options (filters, a share link)
- **Do not use when:** A decision that must be made before continuing → Dialog; A one-line label for an icon → Tooltip; A list of actions → Menu
- **Variants:** `placement`: bottom | top | start | end. Preferred side; flips when there is no room; `tone`: secondary | quiet | ai. Trigger emphasis; ai marks an AI disclosure trigger (used by AILabel)
- **States:** closed, open: Supported (contract docs.states); modal: Not supported: Popovers never block the page; use Dialog
- **Accessibility:** Target: WCAG 2.2 AA; Web role: dialog; Trigger has aria-expanded and aria-haspopup=dialog; the dialog is labelled by its title; Native role: dialog; Rendered in a transparent Modal so screen readers stay inside; Android back closes it
- **Keyboard:** `Enter` OPEN; `Escape` CLOSE; `Tab` Moves focus (within)
- **Do:** If the content needs scrolling, use a Drawer or Dialog; Title in sentence case; content short enough to read without scrolling
- **Tokens:** 67 tokens (component tier, matched by name; inferred): `--base-menu-popover-background`, `--base-menu-popover-border-color`, `--base-menu-popover-border-width`, +64 more

### ProgressBar

Tells people how much of a task is done and that it is still working.

`import { ProgressBar } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Tasks longer than about 2 seconds with measurable progress (uploads, imports); Indeterminate for longer waits with no measure
- **Do not use when:** Short waits under about 2 seconds or inline in a control → Spinner; Content still loading into a layout → Skeleton; Steps of a wizard → Wizard template step indicator
- **Variants:** not defined
- **States:** determinate, indeterminate, complete: Supported (contract docs.states); error: Not supported: Show failure with an Alert next to the bar
- **Accessibility:** Target: WCAG 2.2 AA; Web role: progressbar; aria-valuenow, aria-valuemin, aria-valuemax and aria-valuetext; indeterminate omits the value; Native role: progressbar; aria-valuemin/max/now/text map to accessibilityValue; indeterminate sets aria-busy; Indeterminate animation slows down under reduced motion
- **Keyboard:** not defined
- **Do:** Every ProgressBar has a label; Label says what is happening, with -ing: "Importing tokens"; Value text in the user's terms when possible ("3 of 12 files")
- **Tokens:** 17 tokens (component tier, matched by name; inferred): `--base-progress-bar-label-font-family`, `--base-progress-bar-label-font-size`, `--base-progress-bar-label-font-weight`, +14 more

### ProofDiff

Shows exactly what an agent proposes to change, line by line, in a form editors have trusted for a century: struck out, then written in.

`import { ProofDiff } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Renames, value changes and replacements proposed by an agent or a person, before they are accepted
- **Do not use when:** Code diffs with context lines → a code diff view in the pull request; A history of past changes → Table
- **Variants:** not defined
- **States:** default: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: list; An ordered list named by label. Each line reads "Replace color.brand with color.action.primary.background": the strike-through and the colour are never the only signal; The caret is decorative and hidden from assistive technology; Native role: list; The same words are spoken on native
- **Keyboard:** not defined
- **Do:** When an agent proposed the change, an AILabel sits on the surface that holds the ProofDiff; One replacement per line; Use the exact names people will search for (token names, component names); Pair with a decision: ConfirmAction, or a Seal and Buttons
- **Tokens:** 17 tokens (component tier, matched by name; inferred): `--base-proof-diff-item-gap`, `--base-proof-diff-item-padding-block`, `--base-proof-diff-item-border-block-start-color`, +14 more

### Provider

Gives every Base component the tokens for the right mode, density, motion and locale, and lets a region switch them.

`import { Provider } from "@loom/base/native";` · lifecycle beta · tier foundation

- **Use when:** Once at the root of every app; Nested, to render a region in another mode (a dark preview panel)
- **Do not use when:** Changing one component's colours → A brand layer (themes) or a contract change, never local overrides
- **Variants:** `mode`: light | dark | high-contrast | system. Colour mode. system follows the OS (prefers-color-scheme and prefers-contrast on web, the colour scheme on native); `density`: comfortable | compact. Control sizes and insets; `motion`: standard | reduced | system. system follows prefers-reduced-motion on web; native uses standard unless set to reduced; `aiPresence`: subtle | marked. How strongly AI-generated content is marked; `dir`: ltr | rtl. Text direction
- **States:** light, dark, high-contrast, system, comfortable, compact, reduced motion, AI subtle or marked, any locale: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; Sets lang and dir so screen readers pronounce text correctly (3.1.1, 3.1.2); Native role: none; A theme context: resolves tokens for the chosen contexts at runtime
- **Keyboard:** not defined
- **Do:** Every app renders Base inside a Provider; Set locale from the user's language, not their region
- **Tokens:** 5 tokens (component tier, matched by name; inferred): `--base-provider-background`, `--base-provider-color`, `--base-provider-font-family`, +2 more

### RadioGroup

Asks people to choose exactly one option from a short list they can see all at once.

`import { RadioGroup } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** 2 to about 6 mutually exclusive options; When comparing the options helps the choice
- **Do not use when:** More than about 6 options, or space is tight → Select; Several options can be chosen → CheckboxGroup; A single on/off setting → Switch
- **Variants:** `orientation`: vertical | horizontal. vertical by default; horizontal only for 2 or 3 short options
- **States:** unselected, selected, hover, focus-visible, invalid, disabled (group or option): Supported (contract docs.states); indeterminate: Not supported: Not defined for radios
- **Accessibility:** Target: WCAG 2.2 AA; Web role: radiogroup; Native radio inputs; group labelled by its label, described by hint and error; Native role: radiogroup; Container labelled by its label with hint and error as the accessibility hint; each option is a radio with aria-checked; Arrow keys move and choose (APG radio group); one tab stop
- **Keyboard:** `Tab` Tab enters the group on the chosen option (or the first) and leaves it; `ArrowDown` Moves to and chooses the next option, wrapping; `ArrowUp` SELECT; `Space` SELECT
- **Do:** A RadioGroup has at least two options; Label the group with the question; Options are short, parallel and mutually exclusive; Order options logically (frequency, size), not alphabetically by default
- **Tokens:** 40 tokens (component tier, matched by name; inferred): `--base-radio-group-label-font-family`, `--base-radio-group-label-font-size`, `--base-radio-group-label-font-weight`, +37 more

### Seal

Makes human approval visible and unmistakable: an agent can draft and propose, but only a person's seal passes a change.

`import { Seal } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** On a proposal, next to the action that approves it; In a history of passed changes, to show who approved each one
- **Do not use when:** Status of a running task → AIStatus or Badge; Decorative branding → nothing: the seal always means a real approval
- **Variants:** `status`: awaiting | sealed. awaiting: no person has approved yet. sealed: approved by name on date
- **States:** awaiting, sealed: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: img; role img with an aria-label that says the state in words: "Awaiting a seal" or "Sealed by Ruiz, 24 Oct 2026". Colour and the stamp shape are never the only signal; The stamp animation runs once and only when motion is allowed; Native role: img; View with an accessibility label in the same words
- **Keyboard:** not defined
- **Do:** A sealed Seal is shown only for an approval recorded against a person; Only a person's approval produces a sealed Seal; never an agent's; Name and date always appear together; Keep the name short enough for the seal (a family name or initials)
- **Tokens:** 28 tokens (component tier, matched by name; inferred): `--base-seal-width`, `--base-seal-height`, `--base-seal-padding`, +25 more

### SectionNav

Shows every section of the product in one line, the way a newspaper lists its sections, so people always see where they are and what else exists.

`import { SectionNav } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Under a Masthead, for five to nine top-level sections
- **Do not use when:** More than nine destinations or deep hierarchies → SideNav; Switching views of the same content → Tabs
- **Variants:** not defined
- **States:** default, hover, focus-visible, current: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: navigation; nav landmark with a list of links; aria-current="page" on the current link; The row wraps instead of scrolling, so every section stays visible at 320 px and at 400% zoom; Native role: navigation; Pressable links with role link; the current one is selected
- **Keyboard:** `Tab` Each link is a tab stop
- **Do:** Exactly one item is current; Section names are nouns, one or two words; Counts are numbers only; the label says what they count
- **Tokens:** 20 tokens (component tier, matched by name; inferred): `--base-section-nav-link-outline-color`, `--base-section-nav-link-outline-width`, `--base-section-nav-link-outline-offset`, +17 more

### Select

Lets people choose one option from a list that only takes space when open.

`import { Select } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** One choice from about 5 to 15 known options; Space is tight, so radios would be too long
- **Do not use when:** 2 to 5 options where comparing them matters → RadioGroup; Long lists where typing is faster → ComboBox; Actions rather than a value → Menu
- **Variants:** not defined
- **States:** closed, open, placeholder, chosen, hover, focus, invalid, disabled: Supported (contract docs.states); multiple: Not supported: Multi-select is an expansion item, not in v1
- **Accessibility:** Target: WCAG 2.2 AA; Web role: button; Trigger is a button with aria-haspopup=listbox, labelled by the label and the value; a hidden native select supports form autofill; Native role: button; Trigger announces "value, label"; options are role option in a modal list; Android back closes it; Typeahead on web: typing a letter moves to the matching option
- **Keyboard:** `Enter` OPEN when state(closed); `Space` OPEN when state(closed); `ArrowDown` Opens on the chosen or first option; `ArrowDown` Moves focus (next); `ArrowUp` Moves focus (previous); `Enter` SELECT when state(open); `Escape` CLOSE
- **Do:** Select chooses a value; use Menu for actions; Options are short and start with the distinguishing word; Placeholder says "Choose an option"; never repeats the label; Order options logically; alphabetical only when there is no better order
- **Tokens:** 56 tokens (component tier, matched by name; inferred): `--base-select-gap`, `--base-select-label-font-family`, `--base-select-label-font-size`, +53 more

### SideNav

Lets people move between the main areas of the app and always see where they are.

`import { SideNav } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** The app shell's primary navigation on wide screens
- **Do not use when:** Switching views of one object → Tabs; Actions → Menu or Button
- **Variants:** not defined
- **States:** default, hover, focus-visible, current: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: navigation; nav landmark with a list of links; aria-current="page" on the current link; Native role: navigation; Pressable links with role link; the current one is selected; Current page shown by weight, background and a rule, not colour alone; announced with aria-current (consistent navigation, 3.2.3)
- **Keyboard:** `Tab` Each link is a tab stop
- **Do:** Exactly one item is current; One or two words per destination, nouns; Same order on every page
- **Tokens:** 27 tokens (component tier, matched by name; inferred): `--base-side-nav-link-outline-color`, `--base-side-nav-link-outline-width`, `--base-side-nav-link-outline-offset`, +24 more

### Sidenote

Puts the evidence for a claim next to the claim, so every sentence an agent writes can be checked without leaving the page.

`import { Sidenote } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Agent-written stories and answers that cite records; Any text where a number or claim needs its source
- **Do not use when:** A full list of sources under an answer → SourcesList; Explaining a control → a hint under the field, or Popover
- **Variants:** not defined
- **States:** default: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; A paragraph that starts with its number, so it reads as "1, Figma file Atlas Library..."; Notes follow the text in source order; on narrow screens they fall under it; Native role: none; Text with the number first
- **Keyboard:** not defined
- **Do:** Every Sidenote number has a Citation with the same number in the text; Name the record and its date, then link to it; One record per note
- **Tokens:** 10 tokens (component tier, matched by name; inferred): `--base-sidenote-gap`, `--base-sidenote-padding-block`, `--base-sidenote-color`, +7 more

### Skeleton

Holds the layout steady while content loads, so the page does not jump and people know something is coming.

`import { Skeleton } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Loading content into a known layout (cards, rows, profile headers)
- **Do not use when:** Short waits for an action → Button busy or Spinner; Long tasks with progress → ProgressBar
- **Variants:** `shape`: text | block | circle. text: three lines; block: a card or image area; circle: an avatar
- **States:** text, block, circle: Supported (contract docs.states); shimmer: Not supported: No shimmer animation: calm by default and safe under reduced motion
- **Accessibility:** Target: WCAG 2.2 AA; Web role: status; The shapes are aria-hidden; the status names what is loading. Replace the skeleton with content when ready; Native role: status; View role status with aria-label; shapes are plain Views
- **Keyboard:** not defined
- **Do:** Replace skeletons with content or an error; never leave them after loading fails; label names what is loading
- **Tokens:** 14 tokens (component tier, matched by name; inferred): `--base-skeleton-gap`, `--base-skeleton-line1-background`, `--base-skeleton-line2-background`, +11 more

### SkipLink

Lets keyboard and screen-reader users bypass repeated navigation on every page.

`import { SkipLink } from "@loom/base";` · lifecycle beta · tier component

- **Use when:** Every page that has a header or navigation before the main content (the app shell includes it)
- **Do not use when:** Native apps → Nothing: platform screen readers navigate by headings and landmarks
- **Variants:** not defined
- **States:** hidden, visible on focus: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: link; Off-screen until focused, then shown in the corner above everything (layer.tooltip); Native role: none; No native form: VoiceOver and TalkBack have rotor and headings navigation; 2.4.1 Bypass blocks
- **Keyboard:** `Tab` The first Tab on a page shows it; `Enter` Moves focus to the main region
- **Do:** SkipLink is the first focusable element and its target exists; "Skip to main content"; translate with the UI
- **Tokens:** 14 tokens (component tier, matched by name; inferred): `--base-skip-link-outline-color`, `--base-skip-link-outline-width`, `--base-skip-link-outline-offset`, +11 more

### SourcesList

Shows every source a generated answer used, so people can verify it.

`import { SourcesList } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Below any generated answer that cites sources
- **Do not use when:** Ordinary reference lists → A list of Links
- **Variants:** not defined
- **States:** with or without publisher and provenance: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: list; A heading and an ordered list; each item has the id Citation links point to; Native role: list; Heading Text and link Texts
- **Keyboard:** `Tab` Moves focus (next)
- **Do:** Sources appear in first-citation order; Titles as the source publishes them; publisher after; Provenance names the model or system, the date and the data scope
- **Tokens:** 24 tokens (component tier, matched by name; inferred): `--base-sources-list-link-outline-color`, `--base-sources-list-link-outline-width`, `--base-sources-list-link-outline-offset`, +21 more

### Spinner

Shows that something is happening during a short wait.

`import { Spinner } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Waits under about 2 to 10 seconds with no measurable progress; Inside a region that is refreshing
- **Do not use when:** Content loading into a known layout → Skeleton; Long tasks with measurable progress → ProgressBar; A button doing work → Button busy
- **Variants:** `size`: sm | md | lg. sm inside controls, md inline, lg for a region
- **States:** busy: Supported (contract docs.states); complete: Not supported: Remove the spinner and show the result
- **Accessibility:** Target: WCAG 2.2 AA; Web role: progressbar; Indeterminate progressbar with an accessible name; no value; Native role: progressbar; View role progressbar, aria-busy, containing ActivityIndicator; Rotation slows to 3 s per turn under reduced motion rather than stopping, so the busy state stays visible
- **Keyboard:** not defined
- **Do:** Give the spinner a specific label when more than one can be on screen; Label names what is loading: "Loading releases"
- **Tokens:** 15 tokens (component tier, matched by name; inferred): `--base-spinner-label-font-family`, `--base-spinner-label-font-size`, `--base-spinner-label-font-weight`, +12 more

### Spread

Composes a page like a front page: one thing leads, one thing supports it, and everything else waits below the fold.

`import { Spread } from "@loom/base/native";` · lifecycle beta · tier primitive

- **Use when:** Overview and report pages with one main story and one key figure; An answer beside its proof (variant even); Body text with sidenotes (variant notes)
- **Do not use when:** A grid of equal items → Grid; Forms and settings in one reading column → Stack
- **Variants:** `variant`: front | notes | even. front: lead about two thirds, aside one third under a heavy rule. notes: a narrow aside for sidenotes under a hairline. even: two equal columns, for a question and a proof
- **States:** front, notes, even: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; The aside is a complementary landmark named by asideLabel; Source order equals visual order at every width, so reading and focus order never jump (WCAG 1.3.2, 2.4.3); Native role: none; Views stacked in source order
- **Keyboard:** not defined
- **Do:** Give the aside an asideLabel; The lead says what matters most this time; do not split attention between two leads; Name the aside for what it holds
- **Tokens:** 10 tokens (component tier, matched by name; inferred): `--base-spread-gap`, `--base-spread-top-column-gap`, `--base-spread-top-row-gap`, +7 more

### Stack

Spaces a group of elements evenly with the system's spacing scale, so pages need no custom margins.

`import { Stack } from "@loom/base/native";` · lifecycle beta · tier primitive

- **Use when:** Any vertical list of sections, fields or cards; A row of related items (buttons, tags, avatar and name)
- **Do not use when:** Two-dimensional layouts → Grid; A padded surface → Box or Card; Buttons that belong together → ButtonGroup
- **Variants:** `direction`: column | row. column stacks vertically; row places side by side; `gap`: none | xs | sm | md | lg | xl. Space between items, from the spacing scale; `align`: stretch | start | center | end | baseline. Cross-axis alignment; `justify`: start | center | end | between. Main-axis distribution; `wrap`: nowrap | wrap. Rows wrap onto new lines when space runs out
- **States:** column, row, wrap: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; A plain div: adds no semantics. Reading order follows source order; never reverse it visually; Native role: none; A View; Row stacks wrap rather than overflow at 320 px when wrap is set (1.4.10)
- **Keyboard:** not defined
- **Do:** Do not add margins to Stack children; change gap; Gap comes from the scale; no raw pixel spacing; No content of its own
- **Tokens:** 5 tokens (component tier, matched by name; inferred): `--base-stack-xs-gap`, `--base-stack-sm-gap`, `--base-stack-md-gap`, +2 more

### StoryHeader

Leads a page with the news instead of a label: what happened, the facts behind it and who reported it.

`import { StoryHeader } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Overview and report pages where the headline changes with the state of the system; Answers and proposals that an agent filed
- **Do not use when:** Pages whose title is a stable name (Settings, Components) → PageHeader; Card and section titles → Heading
- **Variants:** not defined
- **States:** default: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; A real heading at headingLevel; kicker and deck are paragraphs around it; The kicker is uppercase through CSS only, so screen readers read it in sentence case; Native role: none; Heading Text with paragraphs
- **Keyboard:** not defined
- **Do:** One h1 per page: either PageHeader or StoryHeader holds it; The headline is a full sentence in plain words, with a full stop; The deck gives numbers, not adjectives; The byline names the agent or person and the evidence; agents always carry an AILabel
- **Tokens:** 27 tokens (component tier, matched by name; inferred): `--base-story-header-gap`, `--base-story-header-kicker-color`, `--base-story-header-kicker-font-family`, +24 more

### StreamingText

Shows a response as it is generated without overwhelming screen-reader users, and lets people stop it.

`import { StreamingText } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Any AI response delivered as a stream
- **Do not use when:** Non-AI loading → Spinner, Skeleton or ProgressBar; Tool calls and long tasks with steps → ProgressBar or a step list (task queue is in the opt-in pack)
- **Variants:** `status`: thinking | streaming | complete | stopped. thinking: nothing yet; streaming: text arriving; complete or stopped: finished
- **States:** thinking, streaming, complete, stopped: Supported (contract docs.states); error: Not supported: Use AIStatus kind=error below the partial text
- **Accessibility:** Target: WCAG 2.2 AA; Web role: status; Text is not live; a separate status announces once on completion or stop. aria-busy while working; Native role: status; Same structure; the status Text is a polite live region; 4.1.3 status messages; 2.2.2 pause, stop, hide (stop control)
- **Keyboard:** `Tab` The stop button is reachable while it works
- **Do:** Never put streaming text itself in a live region; Status words are short: "Thinking", "Response ready", "Stopped"; Stop label says what stops: "Stop generating"
- **Tokens:** 17 tokens (component tier, matched by name; inferred): `--base-streaming-text-body-font-family`, `--base-streaming-text-body-font-size`, `--base-streaming-text-body-font-weight`, +14 more

### Switch

Turns a single setting on or off, taking effect immediately.

`import { Switch } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** A setting that applies as soon as it changes (notifications, a view option); Binary preferences in settings pages
- **Do not use when:** The choice is only applied after pressing Save → Checkbox; Choosing between more than two options → RadioGroup; Accepting terms or confirming a statement → Checkbox
- **Variants:** not defined
- **States:** off, on, hover, focus-visible: Supported (contract docs.states); disabled: Exposed as disabled when disabled; indeterminate: Not supported: A switch is always on or off; use Checkbox for mixed; error: Not supported: Switches take effect immediately; report failures with an Alert
- **Accessibility:** Target: WCAG 2.2 AA; Web role: switch; A native checkbox input with role switch: announced as a switch, on or off; Native role: switch; The hint maps to accessibilityHint, read after the name and state; State is not colour alone: the thumb position changes too (1.4.1)
- **Keyboard:** `Space` TOGGLE; `Tab` A disabled switch is skipped
- **Do:** Do not place a Switch inside a form that needs Save to apply; use Checkbox; Label the setting, not the action: "Email me when a draft needs review"; Do not put "on/off" in the label; the state is announced; Sentence case, no end punctuation
- **Tokens:** 36 tokens (component tier, matched by name; inferred): `--base-switch-track-outline-color`, `--base-switch-track-outline-width`, `--base-switch-track-outline-offset`, +33 more

### Table

Shows structured data people scan, compare and sort.

`import { Table } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Lists of records with several attributes (releases, components, findings); When people compare values across rows
- **Do not use when:** A single list of items with one or two attributes → A list in Stack or Card; Layout of non-tabular content → Grid; Selection, bulk actions, resizing → Data table (expansion item, not in v1)
- **Variants:** `sortDirection`: ascending | descending. Sort direction of sortColumn; `density`: comfortable | compact. Row height
- **States:** default, sorted ascending, sorted descending, row hover, empty, compact: Supported (contract docs.states); row selection: Not supported: Data table expansion item; loading: Not supported: Show Skeleton rows in v1
- **Accessibility:** Target: WCAG 2.2 AA; Web role: grid; React Aria Table renders a grid with columnheader, row and gridcell roles and aria-sort on sorted columns; Native role: table; View roles table, row, columnheader and cell; sort direction is the header's accessibility hint. Native screen readers have limited table navigation; Scrolls horizontally inside its container instead of the page (1.4.10 reflow)
- **Keyboard:** `Tab` One tab stop into the grid; `ArrowDown` Moves between rows and cells (grid navigation); `ArrowUp` Moves focus (previous); `Enter` On a sortable header
- **Do:** Every Table has a label; Column labels are short nouns; units in the label, not the cells; Numbers align to the end (align: "end"); Empty label says why and what to do
- **Tokens:** 37 tokens (component tier, matched by name; inferred): `--base-table-column-font-family`, `--base-table-column-font-size`, `--base-table-column-font-weight`, +34 more

### Tabs

Shows one of several related panels in the same space, so people can switch without leaving the page.

`import { Tabs } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Peer views of one object (Details, Activity, Settings); Content people do not need to compare side by side
- **Do not use when:** Steps in a sequence → The wizard template with a stepper; Navigation between unrelated pages → SideNav; Sections people read in order → Headings, or Disclosure for optional detail
- **Variants:** not defined
- **States:** selected, unselected, hover, focus-visible, disabled tab: Supported (contract docs.states); closable: Not supported: Closable tabs are not in v1
- **Accessibility:** Target: WCAG 2.2 AA; Web role: tablist; Tabs control their panel with aria-controls; the panel is labelled by its tab; Native role: tablist; Tabs are role tab with the selected state; the panel is role tabpanel labelled by the tab; Selected tab is marked by weight, colour and an underline, not colour alone
- **Keyboard:** `Tab` Tab moves into the tab list on the selected tab, then into the panel; `ArrowRight` Moves to and selects the next tab, wrapping (automatic activation); `ArrowLeft` SELECT; `Home` SELECT; `End` SELECT
- **Do:** Use 2 to 6 tabs; One or two words per tab, nouns: "Activity"; Do not change tab labels with counts that update constantly; use a Badge
- **Tokens:** 31 tokens (component tier, matched by name; inferred): `--base-tabs-tab-outline-color`, `--base-tabs-tab-outline-width`, `--base-tabs-tab-outline-offset`, +28 more

### Tag

Shows short labels attached to an item, or values and filters people have applied and can remove.

`import { Tag } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Keywords and categories on cards and rows; Applied filters with remove buttons
- **Do not use when:** Status of an item → Badge; Choosing options → CheckboxGroup or a tags input (expansion item); Navigation → Link
- **Variants:** `tone`: neutral | info | ai. Category colour
- **States:** default, removable, remove hover, remove focus-visible: Supported (contract docs.states); selected: Not supported: Selectable chips are not in v1
- **Accessibility:** Target: WCAG 2.2 AA; Web role: text; Text plus a button named with the tag, so "Remove tokens" is clear out of context; Native role: text; Pressable remove button with role button and the same name; Remove target is 24 × 24 px minimum (2.5.8)
- **Keyboard:** `Tab` The remove button is the only tab stop; `Enter` REMOVE
- **Do:** Remove buttons are named with the tag text; One to three words, lowercase for keywords, sentence case for filters
- **Tokens:** 26 tokens (component tier, matched by name; inferred): `--base-tag-remove-outline-color`, `--base-tag-remove-outline-width`, `--base-tag-remove-outline-offset`, +23 more

### Text

Sets body text in the system's type scale and colour roles so reading stays consistent.

`import { Text } from "@loom/base/native";` · lifecycle beta · tier primitive

- **Use when:** Paragraphs, descriptions, metadata lines
- **Do not use when:** Section titles → Heading; Status messages that need attention → Alert; Field help → The field's hint prop
- **Variants:** `variant`: body | body-lg | body-sm | caption | figure. Size: body for most text, body-lg for lead paragraphs, body-sm for dense areas, caption for metadata, figure for a key number on a dashboard (display face, tabular figures; always next to a label that says what it counts); `tone`: default | subtle | danger | success. Colour role: subtle for secondary text; danger and success only next to an icon or label that says the same
- **States:** default: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: paragraph; A p element; Native role: text; A Text element; nested Text keeps inline links; Subtle text meets 4.5:1 on canvas, surface and sunken in every mode
- **Keyboard:** not defined
- **Do:** Use tone for colour; never style Text with raw colours; Keep lines under about 72 characters (the component caps the measure); Never use colour alone to convey meaning: pair danger and success tones with words or icons; No italics for Japanese emphasis; use strong
- **Tokens:** 17 tokens (component tier, matched by name; inferred): `--base-text-font-family`, `--base-text-font-weight`, `--base-text-line-height`, +14 more

### TextArea

Collects longer free text: descriptions, notes, feedback.

`import { TextArea } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Answers that may need more than one line; Descriptions and comments
- **Do not use when:** A single short value → TextField; Formatted text (bold, lists, links) → A rich-text editor (not part of Base v1)
- **Variants:** not defined
- **States:** default, hover, focus, filled, invalid, disabled, read-only: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: textbox; A <textarea> (aria-multiline implied), linked to label, hint and error; Native role: textbox; Multiline TextInput labelled with aria-label; hint and error as the accessibility hint
- **Keyboard:** `Tab` Tab leaves the field; it does not insert a tab character
- **Do:** Every TextArea has a visible label; Size rows to the expected answer length; Say limits in the hint ("Up to 500 characters")
- **Tokens:** 36 tokens (component tier, matched by name; inferred): `--base-text-area-gap`, `--base-text-area-label-font-family`, `--base-text-area-label-font-size`, +33 more

### TextField

Collects a short piece of text such as a name, an email address or a URL.

`import { TextField } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Short free-text answers on one line; Search boxes (type="search")
- **Do not use when:** More than one line of text → TextArea; Choosing from a known list → Select or ComboBox; Dates and numbers with a fixed format → Typed date input or number input (expansion items, not in v1)
- **Variants:** `type`: text | email | url | tel | search | password. Input type: sets the keyboard on touch devices and browser validation
- **States:** default, hover, focus, filled, invalid, disabled, read-only: Supported (contract docs.states); loading: Not supported: Validation in progress is shown in the hint, not as a field state
- **Accessibility:** Target: WCAG 2.2 AA; Web role: textbox; Label, hint and error are linked with aria-labelledby and aria-describedby; invalid sets aria-invalid; IME composition: Enter during composition does not submit (React Aria handles composition events); Native role: textbox; React Native has no textbox role: the input is labelled with aria-label and hint and error become the accessibility hint; Error identification (3.3.1) and labels (3.3.2) are built in; Invalid border is thicker, not only red (1.4.1)
- **Keyboard:** `Tab` Moves focus (next)
- **Do:** Every TextField has a visible label; Personal-data fields set autoComplete (WCAG 1.3.5); Label is a noun phrase: "Project name". No colon; Never use placeholder text instead of a label or hint; Errors say what to do: "Enter a project name", not "Invalid input"; Mark optional fields "(optional)" when most fields are required, otherwise mark required ones
- **Tokens:** 35 tokens (component tier, matched by name; inferred): `--base-text-field-gap`, `--base-text-field-label-font-family`, `--base-text-field-label-font-size`, +32 more

### ToastRegion

Confirms that something happened without interrupting, in a place screen readers announce politely.

`import { ToastRegion } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Outcome of a quick action: saved, copied, published, undo available
- **Do not use when:** Errors people must act on → Alert or ErrorSummary; Decisions → Dialog
- **Variants:** not defined
- **States:** neutral, success, danger, dismissible: Supported (contract docs.states); auto-dismiss: Not supported: Not in v1: timing must be adjustable (2.2.1); add a timer only with pause on hover and focus
- **Accessibility:** Target: WCAG 2.2 AA; Web role: region; A named region with aria-live=polite; new toasts are announced once; Native role: region; aria-live polite (Android live region); iOS reads new content when focused; 4.1.3 status messages without focus change
- **Keyboard:** `Tab` Dismiss buttons are reachable; toasts never take focus
- **Do:** One ToastRegion per app; Past tense, object first: "Release 2.4 published"; One line; an Undo action belongs in a Button inside the description slot (v2)
- **Tokens:** 25 tokens (component tier, matched by name; inferred): `--base-toast-region-dismiss-outline-color`, `--base-toast-region-dismiss-outline-width`, `--base-toast-region-dismiss-outline-offset`, +22 more

### Tooltip

Names or briefly explains a control, mostly icon-only buttons, without cluttering the interface.

`import { Tooltip } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** Icon-only buttons: show the label sighted users cannot read; A keyboard shortcut or one-line clarification
- **Do not use when:** Essential or long information → Hint text, or Popover with a heading; Interactive content (links, buttons) → Popover; Validation errors → The field's error message
- **Variants:** `placement`: top | bottom | start | end. Preferred side; it flips when there is no room
- **States:** hidden, visible: Supported (contract docs.states); interactive: Not supported: Tooltips never contain controls
- **Accessibility:** Target: WCAG 2.2 AA; Web role: tooltip; The trigger is described by the tooltip (aria-describedby) while it is visible; Native role: tooltip; The content is the trigger's accessibility hint, so screen-reader users always get it; a long press shows the bubble for sighted users; Hoverable, dismissible with Escape and persistent while hovered (1.4.13)
- **Keyboard:** `Tab` Focus shows it; `Escape` HIDE
- **Do:** Tooltip content is never the only place essential information appears; A phrase, not a sentence; no end punctuation; Do not repeat the visible label
- **Tokens:** 11 tokens (component tier, matched by name; inferred): `--base-tooltip-background`, `--base-tooltip-color`, `--base-tooltip-border-radius`, +8 more

### VisuallyHidden

Adds words that make an interface clear to screen-reader users when the visual design already makes it clear to sighted users.

`import { VisuallyHidden } from "@loom/base/native";` · lifecycle beta · tier primitive

- **Use when:** Extra context for links and buttons ("Edit Atlas"); Headings for regions that are visually obvious
- **Do not use when:** Content everyone needs → Show it; Naming an icon-only control → The control's own label (Button children with iconOnly)
- **Variants:** not defined
- **States:** hidden: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: none; Clipped to 1px, still in the accessibility tree; Native role: text; A zero-opacity, 1px Text: still read by VoiceOver and TalkBack
- **Keyboard:** not defined
- **Do:** No focusable children; Keep it short; it is read in line
- **Tokens:** not defined

### WarpRail

Makes Loom's operating model the navigation itself: everything you do is on one of four threads (See, Shape, Govern, Run), and you can always see which thread you are on.

`import { WarpRail } from "@loom/base/native";` · lifecycle beta · tier component

- **Use when:** The Selvedge shell, in AppShell's nav slot with frame="rail"; Any product whose sections group into a few named streams of work
- **Do not use when:** More than five groups or deep trees → SideNav; A few peer sections in a publication layout → SectionNav
- **Variants:** not defined
- **States:** default, current thread, current section, narrow (bottom bar): Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: navigation; nav with nested lists of links; aria-current on the current page and thread; Native role: navigation; Pressable links; the current ones are selected; A nav landmark of ordinary links in reading order: Tab moves thread by thread, section by section; Enter follows a link; The current page has aria-current="page"; its thread has aria-current="true". The knot and the thick thread repeat it visually; Each section list is named by its thread, so screen readers announce "See, list, 3 items"; Thread colours pass 3:1 on the ink rail; every link has a cotton focus ring; Under 48rem the rail becomes a sticky bottom bar: four thread tabs, with the current thread's sections in a row above them. Nothing is hidden behind a menu, and the bar sits after the main content in reading order
- **Keyboard:** `Tab` Each thread and section link is a tab stop; `Enter` Follows the link
- **Do:** Exactly one section is current, and its thread is marked current; Four threads in the order See, Shape, Govern, Run; Thread names stay one word; sections one or two words; Mark exactly one current section and its thread
- **Tokens:** 68 tokens (component tier, matched by name; inferred): `--base-warp-rail-background`, `--base-warp-rail-padding-block`, `--base-warp-rail-gap`, +65 more

### WeaveDraft

Shows at a glance how far a proposed change reaches, in the notation weavers use for a draft, so a reviewer sees the footprint before reading the diff.

`import { WeaveDraft } from "@loom/base/native";` · lifecycle beta · tier pattern

- **Use when:** With an AI or human proposal: renames, token swaps, deprecations; Any changes × targets matrix with a yes/no per cell
- **Do not use when:** Line-by-line changes → ProofDiff; Health per cell → Cloth
- **Variants:** not defined
- **States:** touched, not touched: Supported (contract docs.states)
- **Accessibility:** Target: WCAG 2.2 AA; Web role: table; table with a caption and header scopes; each cell carries its state in words; Native role: table; Views with table roles; cells are named; A real table: each cell is announced with its change and component and the words Touches or Not touched; Touched cells differ by a woven glyph and a heavier edge, not only by colour
- **Keyboard:** `Tab` Moves to the scroll region
- **Do:** Each row has one cell per column; Rows: the changes, numbered to match the list beside the draft; Columns: components or files, short names; Keep it to about 16 columns; group the rest
- **Tokens:** 27 tokens (component tier, matched by name; inferred): `--base-weave-draft-outline-color`, `--base-weave-draft-outline-width`, `--base-weave-draft-outline-offset`, +24 more

## Do's and Don'ts

Rules from loom/ records in the system repository, component contracts (80 rules in docs.rules), plus Loom's built-in rules. Accepted error rules block; proposed rules only report.

### System rules

- **Every AI-generated content block carries an AILabel (EU AI Act Art. 50).** (`ai-label/always`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: components marked ai-generated without AILabel. Written in `contracts/ai-label.contract.json#ai-label/always`.
- **AI colours (color.ai.*) are locked; brand layers cannot change them.** (`ai-label/locked-colour`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: token build: lock levels. Written in `contracts/ai-label.contract.json#ai-label/locked-colour`.
- **Refusals and errors offer at least one next step or retry.** (`ai-status/next-step`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint. Written in `contracts/ai-status.contract.json#ai-status/next-step`.
- **Do not make danger alerts dismissible when the problem still exists.** (`alert/no-dismiss-errors`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: tone=danger with dismissible. Written in `contracts/alert.contract.json#alert/no-dismiss-errors`.
- **One AppHeader per page.** (`app-header/one`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: axe: landmark-no-duplicate-banner. Written in `contracts/app-header.contract.json#app-header/one`.
- **Pages use AppShell so the skip link, banner, navigation and main landmarks exist.** (`app-shell/landmarks`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: axe: region, landmark rules. Written in `contracts/app-shell.contract.json#app-shell/landmarks`.
- **Avatar always has a name.** (`avatar/name`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: contract: name is required. Written in `contracts/avatar.contract.json#avatar/name`.
- **The text names the status; tone only reinforces it.** (`badge/not-colour-alone`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/badge.contract.json#badge/not-colour-alone`.
- **Exactly one band is current.** (`band-scale/one-current`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: count of current items. Written in `contracts/band-scale.contract.json#band-scale/one-current`.
- **No style overrides on Box; every option is a token role.** (`box/tokens-only`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint. Written in `contracts/box.contract.json#box/tokens-only`.
- **Exactly one item is current and it is the last.** (`breadcrumbs/last-current`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint. Written in `contracts/breadcrumbs.contract.json#breadcrumbs/last-current`.
- **At most one primary Button in a group.** (`button-group/one-primary`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint. Written in `contracts/button-group.contract.json#button-group/one-primary`.
- **At most one tone="primary" Button per view or dialog.** (`button/one-primary`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: count primary buttons per page and dialog. Written in `contracts/button.contract.json#button/one-primary`.
- **Do not navigate with Button; use Link for navigation.** (`button/no-link-semantics`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: onPress handlers that only call router.push or location.assign. Written in `contracts/button.contract.json#button/no-link-semantics`.
- **iconOnly buttons keep a descriptive children label.** (`button/icon-only-name`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: contract: children is required. Written in `contracts/button.contract.json#button/icon-only-name`.
- **Do not nest Cards; use sections or Disclosure inside.** (`card/no-nested-cards`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: Card inside Card. Written in `contracts/card.contract.json#card/no-nested-cards`.
- **Every CheckboxGroup has a label.** (`checkbox-group/label`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: contract: label is required. Written in `contracts/checkbox-group.contract.json#checkbox-group/label`.
- **Two or more checkboxes answering one question go in a CheckboxGroup with a label.** (`checkbox/group`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: sibling Checkbox components without a group. Written in `contracts/checkbox.contract.json#checkbox/group`.
- **Every Citation index exists in the SourcesList on the same answer.** (`citation/has-source`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint. Written in `contracts/citation.contract.json#citation/has-source`.
- **Every cell has an accessible name with family, product, band and count.** (`cloth/name`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: cell.name present. Written in `contracts/cloth.contract.json#cloth/name`.
- **Health is never shown by colour alone: each band has its own weave shape, glyph and words.** (`cloth/not-colour-alone`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/cloth.contract.json#cloth/not-colour-alone`.
- **Each row has one cell per product, in column order.** (`cloth/columns`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: cells.length === products.length. Written in `contracts/cloth.contract.json#cloth/columns`.
- **Tone follows the thread the figure belongs to; never use madder only to alarm.** (`colour-block/tone-meaning`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/colour-block.contract.json#colour-block/tone-meaning`.
- **Prefer ComboBox over Select above 15 options.** (`combo-box/long-lists`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: Select items length. Written in `contracts/combo-box.contract.json#combo-box/long-lists`.
- **AI actions that write data go through ConfirmAction unless the area is in Full autonomy.** (`confirm-action/required`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review + agent eval. Written in `contracts/confirm-action.contract.json#confirm-action/required`.
- **Offer undo whenever the change can be reverted.** (`confirm-action/undo`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/confirm-action.contract.json#confirm-action/undo`.
- **Destructive confirmations use variant="alert" and a danger Button.** (`dialog/destructive-alert`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/dialog.contract.json#dialog/destructive-alert`.
- **Do not open a Dialog from inside a Dialog.** (`dialog/no-nesting`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/dialog.contract.json#dialog/no-nesting`.
- **headingLevel follows the page outline.** (`disclosure/heading-level`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/disclosure.contract.json#disclosure/heading-level`.
- **Prefer spacing; use dividers only where groups would otherwise blur.** (`divider/sparing`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/divider.contract.json#divider/sparing`.
- **One drawer at a time; never stack drawers.** (`drawer/one`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/drawer.contract.json#drawer/one`.
- **actionLabel starts with a verb.** (`empty-state/action-verb`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: copy lint. Written in `contracts/empty-state.contract.json#empty-state/action-verb`.
- **Every error id matches an input id on the page.** (`error-summary/ids`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: Playwright: link targets exist. Written in `contracts/error-summary.contract.json#error-summary/ids`.
- **Say what feedback is stored and who sees it in the report Dialog.** (`feedback/privacy`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/feedback-controls.contract.json#feedback/privacy`.
- **FormField wraps exactly one control.** (`form-field/one-control`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/form-field.contract.json#form-field/one-control`.
- **Do not give Grid cells fixed widths.** (`grid/no-fixed-widths`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/grid.contract.json#grid/no-fixed-widths`.
- **One level-1 Heading per page.** (`heading/one-h1`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: axe: page-has-heading-one; lint. Written in `contracts/heading.contract.json#heading/one-h1`.
- **Do not skip levels (h2 to h4).** (`heading/no-skip`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: axe: heading-order. Written in `contracts/heading.contract.json#heading/no-skip`.
- **One HealthBand per page, holding the h1.** (`health-band/one`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: axe: page-has-heading-one. Written in `contracts/health-band.contract.json#health-band/one`.
- **name is an icon in the Base set.** (`icon/known-name`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: against icons.json. Written in `contracts/icon.contract.json#icon/known-name`.
- **Link text is not "click here", "here", "more" or a bare URL.** (`link/descriptive-text`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: children against the vague-link word list. Written in `contracts/link.contract.json#link/descriptive-text`.
- **Do not use Link with an onPress that performs an action and no navigation; use Button.** (`link/no-action`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/link.contract.json#link/no-action`.
- **Every thread names where it frays and its band in words.** (`loose-threads/where`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: where and bandLabel present. Written in `contracts/loose-threads.contract.json#loose-threads/where`.
- **One Masthead (or AppHeader) per page.** (`masthead/one`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: axe: landmark-no-duplicate-banner. Written in `contracts/masthead.contract.json#masthead/one`.
- **A danger item opens a Dialog variant="alert" before acting.** (`menu/danger-confirms`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/menu.contract.json#menu/danger-confirms`.
- **At most one Numeral per page.** (`numeral/one`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: count of Numeral. Written in `contracts/numeral.contract.json#numeral/one`.
- **One PageHeader with headingLevel 1 per page.** (`page-header/one-h1`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: axe: page-has-heading-one. Written in `contracts/page-header.contract.json#page-header/one-h1`.
- **With two paginations on a page, give each a distinct label.** (`pagination/landmark-name`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/pagination.contract.json#pagination/landmark-name`.
- **If the content needs scrolling, use a Drawer or Dialog.** (`popover/size`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/popover.contract.json#popover/size`.
- **Every ProgressBar has a label.** (`progress/label`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: contract: label is required. Written in `contracts/progress-bar.contract.json#progress/label`.
- **When an agent proposed the change, an AILabel sits on the surface that holds the ProofDiff.** (`proof-diff/ai-label`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: ProofDiff inside an AI-labelled region. Written in `contracts/proof-diff.contract.json#proof-diff/ai-label`.
- **Every app renders Base inside a Provider.** (`provider/root`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: entry point. Written in `contracts/provider.contract.json#provider/root`.
- **A RadioGroup has at least two options.** (`radio/min-two`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: items length. Written in `contracts/radio-group.contract.json#radio/min-two`.
- **A sealed Seal is shown only for an approval recorded against a person.** (`seal/human-only`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review: data source of name and date. Written in `contracts/seal.contract.json#seal/human-only`.
- **Exactly one item is current.** (`section-nav/one-current`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: count of current items. Written in `contracts/section-nav.contract.json#section-nav/one-current`.
- **Select chooses a value; use Menu for actions.** (`select/not-for-actions`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/select.contract.json#select/not-for-actions`.
- **Exactly one item is current.** (`side-nav/one-current`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint. Written in `contracts/side-nav.contract.json#side-nav/one-current`.
- **Every Sidenote number has a Citation with the same number in the text.** (`sidenote/matches-citation`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: citation numbers. Written in `contracts/sidenote.contract.json#sidenote/matches-citation`.
- **Replace skeletons with content or an error; never leave them after loading fails.** (`skeleton/replace`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/skeleton.contract.json#skeleton/replace`.
- **SkipLink is the first focusable element and its target exists.** (`skip-link/first`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: Playwright: first Tab focuses it; target id exists. Written in `contracts/skip-link.contract.json#skip-link/first`.
- **Sources appear in first-citation order.** (`sources/order`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint. Written in `contracts/sources-list.contract.json#sources/order`.
- **Give the spinner a specific label when more than one can be on screen.** (`spinner/name`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/spinner.contract.json#spinner/name`.
- **Give the aside an asideLabel.** (`spread/name-aside`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: axe: landmark-unique, region names. Written in `contracts/spread.contract.json#spread/name-aside`.
- **Do not add margins to Stack children; change gap.** (`stack/no-margins`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: margin on Stack children. Written in `contracts/stack.contract.json#stack/no-margins`.
- **Gap comes from the scale; no raw pixel spacing.** (`stack/no-raw-gap`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: contract: gap is an enum. Written in `contracts/stack.contract.json#stack/no-raw-gap`.
- **One h1 per page: either PageHeader or StoryHeader holds it.** (`story-header/one-h1`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: axe: page-has-heading-one. Written in `contracts/story-header.contract.json#story-header/one-h1`.
- **Never put streaming text itself in a live region.** (`streaming/no-live-text`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: contract: body is not live. Written in `contracts/streaming-text.contract.json#streaming/no-live-text`.
- **Do not place a Switch inside a form that needs Save to apply; use Checkbox.** (`switch/immediate`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/switch.contract.json#switch/immediate`.
- **Every Table has a label.** (`table/label`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: contract: label is required. Written in `contracts/table.contract.json#table/label`.
- **Use 2 to 6 tabs.** (`tabs/count`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: items length. Written in `contracts/tabs.contract.json#tabs/count`.
- **Remove buttons are named with the tag text.** (`tag/remove-name`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: contract: remove name binding. Written in `contracts/tag.contract.json#tag/remove-name`.
- **Every TextArea has a visible label.** (`text-area/label`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: contract: label is required. Written in `contracts/text-area.contract.json#text-area/label`.
- **Every TextField has a visible label.** (`text-field/label`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: contract: label is required. Written in `contracts/text-field.contract.json#text-field/label`.
- **Personal-data fields set autoComplete (WCAG 1.3.5).** (`text-field/autocomplete`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: name/email/tel fields without autoComplete. Written in `contracts/text-field.contract.json#text-field/autocomplete`.
- **Use tone for colour; never style Text with raw colours.** (`text/no-raw-color`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: style props on Text. Written in `contracts/text.contract.json#text/no-raw-color`.
- **One ToastRegion per app.** (`toast-region/one`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint. Written in `contracts/toast-region.contract.json#toast-region/one`.
- **Tooltip content is never the only place essential information appears.** (`tooltip/not-essential`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: review. Written in `contracts/tooltip.contract.json#tooltip/not-essential`.
- **No focusable children.** (`visually-hidden/no-focusable`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint. Written in `contracts/visually-hidden.contract.json#visually-hidden/no-focusable`.
- **Exactly one section is current, and its thread is marked current.** (`warp-rail/current`; accepted, error; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: one current section. Written in `contracts/warp-rail.contract.json#warp-rail/current`.
- **Each row has one cell per column.** (`weave-draft/columns`; accepted, warn; components; owner base-core). Rationale was not recorded in the contract. Enforced by: Manual: As the contract states: lint: cells.length === columns.length. Written in `contracts/weave-draft.contract.json#weave-draft/columns`.

### Lint rules

Lint rules: not defined.

### Content rules

Content rules: not defined.

### Loom built-in checks

- **Use colour tokens, not raw colours** (`loom.raw-colour`; accepted, warn; colour; owner system owner). Raw colours bypass themes, modes and contrast checks. Enforced by: Checker: hard-coded-color.
- **Use spacing and size tokens, not raw values** (`loom.raw-spacing`; accepted, warn; spacing; owner system owner). Raw dimensions drift from the scale and from density modes. Enforced by: Checker: hard-coded-dimension.
- **Use the system component instead of a local copy** (`loom.reimplementation`; accepted, warn; components; owner system owner). Local copies miss fixes, accessibility work and contract changes. Enforced by: Checker: reimplements-system-component.
- **Customise through slots and tokens, not className or style** (`loom.style-override`; accepted, warn; customisation; owner system owner). Product customisation uses slots and overridable tokens within lock levels; className and style overrides are drift (owner decision 25 Oct). Enforced by: Checker: style-override.
- **Migrate away from deprecated items** (`loom.deprecated`; accepted, warn; lifecycle; owner system owner). Deprecated items are removed in the next major once usage reaches zero or the date passes. Enforced by: Checker: uses-deprecated.
- **Do not build new UI on draft or deprecated items** (`loom.lifecycle`; accepted, warn; lifecycle; owner system owner). Drafts are not ready for production and deprecated items are being removed; agents prefer stable over beta (owner decision 25 Oct). Enforced by: Checker: lifecycle-draft, lifecycle-deprecated.
- **Keep the locked steps and required content of governed flows** (`loom.flow-locked`; accepted, warn; flows; owner system owner). A locked step is the reviewed version of a sensitive screen (consent, payment, deletion). Changing it needs an exception or a new version of the flow record. Default warn like every rule; owners promote it to error per area. Enforced by: Checker: flow-locked-step-missing, flow-locked-component-missing, flow-locked-order-changed, flow-locked-prop-changed, flow-locked-step-changed, flow-required-content-missing.
- **Build governed flows from their flow record** (`loom.flow`; accepted, warn; flows; owner system owner). Adaptable steps keep their required components; screens that implement a governed flow say so, so reviewers and agents know which guardrails apply. Enforced by: Checker: flow-adaptable-component-missing, flow-step-order-changed, flow-undeclared, flow-unknown-step, flow-steps-unmarked.
- **Follow the pattern record** (`loom.pattern`; accepted, warn; patterns; owner system owner). Pattern records hold the agreed composition and do/don't rules for a task. Enforced by: Pattern checks.
- **Accessible names and labels** (`loom.accessibility`; accepted, error; accessibility; owner accessibility owner). Accessibility is enforced as a blocking error on every change, even where other rules default to warnings. Enforced by: Checker: a11y-missing-form-label, a11y-missing-alt, a11y-missing-accessible-name.

### Deprecations

Deprecations: not defined.

## Patterns

### App shell and overview

The Selvedge frame (skip link, WarpRail with four threads, slim header, main) with a HealthBand hero whose headline is the band, the system drawn as a Cloth (families by products) with its LooseThreads, three ColourBlock figures and the drafts waiting for a decision. (template, `packages/base/templates/DashboardPage.tsx`)

- **Composes:** AppHeader, AppShell, Badge, BandScale, Button, Cloth, ColourBlock, Grid, Heading, HealthBand, Link, LooseThreads, Menu, Stack, Table, Text, ToastRegion, Tooltip, WarpRail
- One h1 per page, in HealthBand or PageHeader.
- The headline is the health band in words; the score sits in the lede.
- Every cell of the cloth has an accessible name with its family, product, band and open threads; colour is never the only signal.
- Every loose thread links to an action and back to its cell.

### List with filters

An index with search, a status filter, a sortable table, pagination and an empty state for no results. (template, `packages/base/templates/ListPage.tsx`)

- **Composes:** AppHeader, AppShell, Badge, Button, Card, EmptyState, Menu, PageHeader, Pagination, Select, Stack, Table, Tag, TextField, ToastRegion, Tooltip, WarpRail
- Filters sit above the table and change the results in place.
- No results shows EmptyState with a way to clear the filters, never an empty table.
- Sorting is announced through the column header's sort state.

### Detail with drawer

A record page with breadcrumbs, tabs and metadata; related items open in a Drawer without leaving the list context. (template, `packages/base/templates/DetailPage.tsx`)

- **Composes:** AppHeader, AppShell, Avatar, Badge, Breadcrumbs, Button, ButtonGroup, Card, Dialog, Disclosure, Drawer, Grid, Menu, PageHeader, Stack, Tabs, Text, ToastRegion, Tooltip, WarpRail
- Breadcrumbs show the path back to the list.
- Destructive actions confirm in an alert Dialog that names the object.
- The Drawer returns focus to the control that opened it.

### Form with validation

A question page that validates on submit, lists errors in an ErrorSummary at the top and links each error to its field. (template, `packages/base/templates/FormPage.tsx`)

- **Composes:** AppHeader, AppShell, Badge, Button, ButtonGroup, Card, Checkbox, CheckboxGroup, ErrorSummary, Menu, PageHeader, RadioGroup, Select, Stack, TextArea, TextField, ToastRegion, Tooltip, WarpRail
- Validate on submit, not on every keystroke.
- Errors appear in an ErrorSummary at the top, focused on submit, each linking to its field, and again next to the field.
- Every field has a visible label; hints sit between the label and the input.

### Settings

Grouped settings where switches apply at once and other fields are saved with the form. (template, `packages/base/templates/SettingsPage.tsx`)

- **Composes:** AppHeader, AppShell, Badge, Button, ButtonGroup, Card, Disclosure, Divider, Menu, PageHeader, RadioGroup, Select, Stack, Switch, Text, TextField, ToastRegion, Tooltip, WarpRail
- Switches apply immediately; everything else waits for Save.
- Say which changes apply at once in the page description.

### Multi-step wizard

Steps with a progress indicator, validation per step and a review step before the final action. (template, `packages/base/templates/WizardPage.tsx`)

- **Composes:** Alert, AppHeader, AppShell, Badge, Button, ButtonGroup, Card, CheckboxGroup, ErrorSummary, Heading, Menu, PageHeader, ProgressBar, RadioGroup, Stack, Text, TextField, ToastRegion, Tooltip, WarpRail
- Show the current step and the total.
- Back never loses entered data.
- The last step reviews every answer with a way to change it.

### Sign in

Accessible authentication: labelled fields, paste and password managers work, errors are announced. (template, `packages/base/templates/AuthPages.tsx`)

- **Composes:** Alert, AppHeader, AppShell, Badge, Box, Button, Card, Checkbox, HealthBand, Link, Menu, Stack, Text, TextField, ToastRegion, Tooltip, WarpRail
- No cognitive tests (WCAG 3.3.8): paste and password managers must work.
- A failed sign-in says what to do next without revealing which field was wrong.

### Empty page

A first-use state that explains what will appear and offers the first action. (template, `packages/base/templates/StatusPages.tsx`)

- **Composes:** AppHeader, AppShell, Badge, Box, Button, ButtonGroup, Card, EmptyState, Heading, Icon, Link, Menu, PageHeader, Stack, Text, ToastRegion, Tooltip, WarpRail
- Explain what will appear here and offer one next action.

### Error pages

Not-found and service-error pages that say what happened and how to recover. (template, `packages/base/templates/StatusPages.tsx`)

- **Composes:** AppHeader, AppShell, Badge, Box, Button, ButtonGroup, Card, EmptyState, Heading, Icon, Link, Menu, PageHeader, Stack, Text, ToastRegion, Tooltip, WarpRail
- Say what happened in plain words, then how to recover; no error codes as the headline.

### Confirmation

The result of a completed task with what happens next. (template, `packages/base/templates/StatusPages.tsx`)

- **Composes:** AppHeader, AppShell, Badge, Box, Button, ButtonGroup, Card, EmptyState, Heading, Icon, Link, Menu, PageHeader, Stack, Text, ToastRegion, Tooltip, WarpRail
- Confirm what was done and what happens next; offer the obvious next task.

### Assistant answer

The proof desk: an AI answer using all six AI interface patterns (label, streaming text, citations, sources, feedback, confirm-before-acting and status), with the proposal shown as a weaving draft (ProofDiff and a WeaveDraft grid of the components each change touches). (template, `packages/base/templates/AssistantPage.tsx`)

- **Composes:** AILabel, AIStatus, AppHeader, AppShell, Badge, Button, Card, Citation, ConfirmAction, FeedbackControls, Grid, Heading, HealthBand, Menu, ProofDiff, SourcesList, Stack, StreamingText, Text, ToastRegion, Tooltip, WarpRail, WeaveDraft
- Every AI-generated block carries an AILabel.
- Changes proposed by AI are shown with ProofDiff, confirmed with ConfirmAction and can be undone.
- Nothing is applied until a person confirms it.
- Answers cite their sources inline and list them under the answer.

## Brand and Voice

### Brand

Status in the records: missing.

- Not defined: Loom Base has no dedicated brand guideline. Do not invent one: follow existing foundations, and use propose_change (kind "guideline") to draft it for review.
- Not defined: No "principles" foundation is defined for this system.
- Not defined: No "icons" foundation is defined for this system.
- Not defined: No brand or voice-and-tone document is included; brand is expressed only through principles, colour, type and shape.

### Voice and content

Status in the records: missing.

- Not defined: Loom Base has no dedicated content guideline. Do not invent one: follow existing foundations, and use propose_change (kind "guideline") to draft it for review.
- Not defined: No "content" foundation is defined for this system.
- Not defined: No "internationalization" foundation is defined for this system.

Content rules enforced on UI copy: not defined.

## Using This System with Agents

This file is a snapshot of the records. The MCP server reads the live records; when the two disagree, the MCP server is right.

- **MCP server:** run Loom's server for this system with `npm run mcp -- --project loom-base` (stdio; add `--http <port>` and a bearer token for HTTP).
- **Before writing UI:** `search_system`, then `get_entity` for exact imports, props, states and accessibility. Use `get_tokens` instead of literal colours or pixel values, and `get_guidelines` and `get_rules` for content and governance.
- **Before finishing:** validate with `check_composition` and `audit_code`. If the system lacks something, draft it with `propose_change` instead of building it off-system.
- **This file:** `get_design_md` returns it from the same records.
- **llms.txt:** `docs/llms.txt` (relative to the system's folder).
