{
  "$schema": "./tokens.schema.json",
  "version": "1.4.0",
  "sourceFile": "D:/01 - projets/SmartStack.app/features/customisation-ui/web/smartstack-web/src/base.css",
  "extractedAt": "2026-04-22",
  "description": "Design tokens + component classes + hard rules. BASELINE reference for ui-polish — at runtime the engine derives a project-aware capability set (theme-capabilities.ts: helper classes + tokens scanned from the TARGET project's CSS, components scanned from src/components). Rules flag bypass of primitives the project ships (R21/R22) and never require components it lacks (R1/R11 are project-aware).",
  "cssVariables": {
    "radius": {
      "card": "12px",
      "button": "8px",
      "badge": "8px",
      "input": "8px",
      "modal": "16px",
      "menuItem": "8px"
    },
    "colorPrimary": {
      "50": "#eef2ff",
      "100": "#e0e7ff",
      "200": "#c7d2fe",
      "300": "#a5b4fc",
      "400": "#818cf8",
      "500": "#6366f1",
      "600": "#4f46e5",
      "700": "#4338ca",
      "800": "#3730a3",
      "900": "#312e81",
      "950": "#1e1b4b"
    },
    "colorAccent": {
      "50": "#f5f3ff",
      "100": "#ede9fe",
      "200": "#ddd6fe",
      "300": "#c4b5fd",
      "400": "#a78bfa",
      "500": "#8b5cf6",
      "600": "#7c3aed",
      "700": "#6d28d9",
      "800": "#5b21b6",
      "900": "#4c1d95",
      "950": "#2e1065"
    },
    "backgrounds": {
      "light": {
        "bgApp": "#fafafa",
        "bgPrimary": "#ffffff",
        "bgSecondary": "#f4f4f5",
        "bgTertiary": "#e4e4e7",
        "bgCard": "#ffffff",
        "bgHover": "#f4f4f5",
        "bgActive": "#e4e4e7"
      },
      "dark": {
        "bgApp": "#161618",
        "bgPrimary": "#1c1c1f",
        "bgSecondary": "#232326",
        "bgTertiary": "#2a2a2e",
        "bgCard": "#1c1c1f",
        "bgHover": "#27272a",
        "bgActive": "#3f3f46"
      }
    },
    "text": {
      "light": {
        "textPrimary": "#18181b",
        "textSecondary": "#52525b",
        "textTertiary": "#71717a",
        "textMuted": "#a1a1aa",
        "textInverse": "#fafafa"
      },
      "dark": {
        "textPrimary": "#e4e4e7",
        "textSecondary": "#a1a1aa",
        "textTertiary": "#71717a",
        "textMuted": "#52525b",
        "textInverse": "#e4e4e7"
      }
    },
    "borders": {
      "light": {
        "borderColor": "#e4e4e7",
        "borderSubtle": "#f4f4f5",
        "borderStrong": "#d4d4d8"
      },
      "dark": {
        "borderColor": "#3f3f46",
        "borderSubtle": "#27272a",
        "borderStrong": "#52525b"
      }
    },
    "status": {
      "success": {
        "bg": "#f0fdfa",
        "text": "#0f766e",
        "border": "#99f6e4",
        "dot": "#14b8a6"
      },
      "warning": {
        "bg": "#fffbeb",
        "text": "#b45309",
        "border": "#fde68a",
        "dot": "#f59e0b"
      },
      "error": {
        "bg": "#fff1f2",
        "text": "#be123c",
        "border": "#fecdd3",
        "dot": "#f43f5e"
      },
      "info": {
        "bg": "#f0f9ff",
        "text": "#0369a1",
        "border": "#bae6fd",
        "dot": "#0ea5e9"
      }
    },
    "accentSoft": {
      "_note": "Named soft-accent surface (--accent-bg/--accent-text/--accent-border), e.g. used by .badge-accent. The CONFORMANT alternative to ad-hoc accent opacity tints (R22). Shipped by the platform theme; values below are the reference baseline (light/dark) — the engine reads the actual names from the target project's CSS.",
      "light": {
        "bg": "#eef2ff",
        "text": "#4338ca",
        "border": "#c7d2fe"
      },
      "dark": {
        "bg": "#312e81",
        "text": "#c7d2fe",
        "border": "#4338ca"
      }
    },
    "layout": {
      "_note": "Page-width contract (R25). Responsive, rem-based. --content-max-narrow caps the 'focused' page mode; --content-gutter-x is the left/right gutter so full-width content clears the edge chrome (doc launcher, sidebar collapse handle). Emitted by scaffold-theme.",
      "contentMaxNarrow": "42rem",
      "contentGutterX": "3rem"
    },
    "fontSans": "'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
  },
  "componentClasses": {
    "card": {
      "description": "Surface card, solid background, shadow-sm",
      "class": ".card"
    },
    "cardElevated": {
      "description": "Elevated card with shadow-md",
      "class": ".card-elevated"
    },
    "btn": {
      "description": "Base button, radius-button, inline-flex, 0.5rem 1rem padding",
      "class": ".btn"
    },
    "btnPrimary": {
      "description": "Primary CTA — accent-600 bg, white text",
      "class": ".btn .btn-primary"
    },
    "btnSecondary": {
      "description": "Secondary — bg-secondary, text-primary, border",
      "class": ".btn .btn-secondary"
    },
    "btnSm": {
      "description": "Smaller padding variant",
      "class": ".btn .btn-sm"
    },
    "btnGhost": {
      "description": "Transparent bg, hover bg-hover",
      "class": ".btn .btn-ghost"
    },
    "input": {
      "description": "Form input, radius-input, focus ring accent-500",
      "class": ".input"
    },
    "focusRing": {
      "description": "Accessibility focus ring — 3px accent-border box-shadow",
      "class": ".focus-ring"
    },
    "badgeSuccess": {
      "class": ".badge-success"
    },
    "badgeWarning": {
      "class": ".badge-warning"
    },
    "badgeError": {
      "class": ".badge-error"
    },
    "badgeInfo": {
      "class": ".badge-info"
    },
    "badgeAccent": {
      "class": ".badge-accent"
    },
    "hoverCard": {
      "description": "Translate -2px + shadow-lg on hover",
      "class": ".hover-card"
    },
    "itemLight": {
      "class": ".item-light"
    },
    "itemMedium": {
      "class": ".item-medium"
    },
    "itemDark": {
      "class": ".item-dark"
    },
    "itemBorder": {
      "class": ".item-border"
    },
    "glass": {
      "description": "Backdrop blur 10px + transparent bg",
      "class": ".glass"
    },
    "gradientText": {
      "description": "135deg gradient primary→accent, -webkit-background-clip: text",
      "class": ".gradient-text"
    },
    "animatedGradient": {
      "class": ".animated-gradient"
    },
    "tooltipAnimation": {
      "class": ".tooltip-animation"
    }
  },
  "sharedComponents": {
    "PageTemplate": {
      "importFrom": "@/components/ui/PageTemplate",
      "props": {
        "title": {
          "type": "string",
          "required": true
        },
        "subtitle": {
          "type": "string | null",
          "required": false
        },
        "icon": {
          "type": "ReactNode (lucide-react icon)",
          "required": false
        },
        "actions": {
          "type": "ReactNode (buttons)",
          "required": false
        },
        "breadcrumbs": {
          "type": "BreadcrumbItem[]",
          "required": false
        },
        "children": {
          "type": "ReactNode",
          "required": true
        },
        "contentClassName": {
          "type": "string",
          "default": "space-y-6"
        }
      },
      "rule": "Every page MUST wrap its content in <PageTemplate>. No bare <div> or <main> at the page root."
    },
    "PageHeader": {
      "importFrom": "@/components/ui/PageHeader",
      "note": "Used internally by PageTemplate. Pages should NOT use PageHeader directly."
    },
    "DataTable": {
      "importFrom": "@/components/ui/DataTable",
      "props": [
        "data",
        "columns",
        "loading",
        "searchable",
        "searchPlaceholder",
        "pagination",
        "onRowClick",
        "getRowKey",
        "emptyMessage",
        "emptyIcon",
        "headerActions",
        "striped",
        "compact",
        "selectable",
        "selectedKeys",
        "onSelectionChange",
        "searchFilter",
        "defaultSortKey",
        "defaultSortDirection"
      ],
      "rule": "List views of entities MUST use DataTable, not raw <table>. Exception: list pages with complex nested sections (like PresetsListPage grouping by theme) may use raw <table> INSIDE a .card section for each group."
    },
    "EntityCard": {
      "importFrom": "@/components/ui/EntityCard",
      "props": [
        "avatar",
        "title",
        "subtitle",
        "description",
        "stats",
        "badge",
        "links",
        "actions",
        "tags",
        "onClick",
        "customHeader",
        "customBody",
        "customFooter"
      ],
      "rule": "Grid-of-cards views (dashboards, gallery lists) use EntityCard."
    },
    "Loader2": {
      "importFrom": "lucide-react",
      "rule": "Loading indicator MUST be Loader2 with animate-spin. No custom spinners."
    }
  },
  "extensionPattern": {
    "slotsPerEntity": [
      "{entity}.header.actions",
      "{entity}.table.columns.before",
      "{entity}.table.columns.after",
      "{entity}.form.fields.before",
      "{entity}.form.fields.after",
      "{entity}.card.content",
      "{entity}.detail.header",
      "{entity}.detail.sidebar"
    ],
    "hooks": [
      "onPageLoad",
      "onBeforeSave",
      "onAfterSave",
      "onFormSubmit"
    ],
    "source": "D:/01 - projets/SmartStack.app/features/customisation-ui/web/smartstack-web/src/extensions/README.md"
  },
  "hardRules": [
    {
      "id": "R1-pagetemplate",
      "description": "Every page MUST wrap its content in <PageTemplate> from @/components/ui/PageTemplate.",
      "violation": "Page returns bare <div> or <main> at the root instead of <PageTemplate>.",
      "autoFixable": true,
      "severity": "error"
    },
    {
      "id": "R2-no-hex-colors",
      "description": "Color values in className MUST use var(--token-name) syntax. Inline style='background: #xxxxxx' is allowed only for dynamically computed accent shades.",
      "violation": "Hex hex values in className attributes (e.g. className=\"bg-[#6366f1]\"). Only var(--…) allowed.",
      "autoFixable": false,
      "severity": "warning"
    },
    {
      "id": "R3-primary-cta",
      "description": "Primary CTA buttons MUST use the .btn .btn-primary helper class — the theme owns the accent fill, hover, text colour, radius and padding. A raw Tailwind colour on a button (bg-blue-500, …) is fixed by switching it to the helper, not by swapping in an inline accent token (re-assembling the CTA inline is itself a violation — see R21).",
      "violation": "Primary action uses an arbitrary colour on a <button> (bg-blue-500, bg-[#6366f1], …) instead of the .btn .btn-primary helper.",
      "autoFixable": true,
      "severity": "error"
    },
    {
      "id": "R4-i18n-namespace",
      "description": "All user-facing text MUST flow through useTranslation('<module|entity>'). No hardcoded strings in JSX.",
      "violation": "JSX contains hardcoded string literal (matched by /<[A-Z][a-zA-Z]+[^>]*>[^<{]+[a-z]/ heuristic).",
      "autoFixable": false,
      "severity": "error"
    },
    {
      "id": "R5-lucide-icons",
      "description": "Icons MUST be imported from lucide-react. No svg fragments, no heroicons, no custom icon packs.",
      "violation": "Import from @heroicons, react-icons, or raw <svg> in JSX.",
      "autoFixable": false,
      "severity": "warning"
    },
    {
      "id": "R6-loader2-spinner",
      "description": "Loading state MUST render <Loader2 className=\"animate-spin\" /> from lucide-react. No custom spinner components.",
      "violation": "Uses a non-Loader2 spinner or CSS-only spinner instead.",
      "autoFixable": false,
      "severity": "warning"
    },
    {
      "id": "R7-error-banner-anatomy",
      "description": "Error banners MUST use bg-[var(--error-bg)] border border-[var(--error-border)] text-[var(--error-text)] rounded-[var(--radius-card)]. No Tailwind red-* utilities.",
      "violation": "Error banner uses bg-red-*, text-red-*, border-red-* utilities directly.",
      "autoFixable": true,
      "severity": "error"
    },
    {
      "id": "R8-datatable-for-lists",
      "description": "List views SHOULD use <DataTable> from @/components/ui/DataTable instead of raw <table>. Exception: nested tables inside .card sections (grouped lists like PresetsListPage).",
      "violation": "A list view renders <table> at the top level when a flat list could use <DataTable>.",
      "autoFixable": false,
      "severity": "info"
    },
    {
      "id": "R9-useparams-nullcheck",
      "description": "useParams<{ id: string }>() result MUST be null-checked. If id is missing, navigate back to the parent list route.",
      "violation": "const { id } = useParams(); api.get(`/x/${id}`); — missing !id guard + navigate.",
      "autoFixable": false,
      "severity": "error"
    },
    {
      "id": "R10-permission-key-format",
      "description": "PermissionGuard keys use format {module}.{section}.{action} (read/create/update/delete). NO appCode prefix.",
      "violation": "permission=\"myapp.hrm.employees.read\" (appCode prefix forbidden) or permission=\"hrm_employees_read\" (underscores forbidden).",
      "autoFixable": true,
      "severity": "error"
    },
    {
      "id": "R11-slot-anatomy",
      "description": "Pages SHOULD expose Slot/Fill injection points: {entity}.header.actions, {entity}.table.columns.before/after, {entity}.form.fields.before/after, {entity}.detail.header/sidebar, {entity}.card.content.",
      "violation": "Generated page does not import Slot from @atlashub/smartstack or does not render any slot.",
      "autoFixable": true,
      "severity": "info"
    },
    {
      "id": "R12-darktheme-base",
      "description": "Dark theme MUST reference var(--bg-app) which resolves to #161618, NOT #0a0a0a or pure black. This is a design-system hard rule.",
      "violation": "Inline dark mode color #000000 or #0a0a0a.",
      "autoFixable": false,
      "severity": "warning"
    },
    {
      "id": "R13-badge-anatomy",
      "description": "Status badges use inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider rounded bg-[var(--<status>-bg)] text-[var(--<status>-text)] border border-[var(--<status>-border)]. Or the .badge-<status> helper class.",
      "violation": "Status badge uses bg-green-500 text-white or similar Tailwind arbitrary colors.",
      "autoFixable": true,
      "severity": "warning"
    },
    {
      "id": "R14-no-hardcoded-colors",
      "description": "Saturated/semantic colors MUST use design tokens — anywhere in className, not only buttons/badges/banners. Every Tailwind brand/status color utility (red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose) on any prefix (bg, text, border, ring, fill, stroke, outline, divide, decoration, placeholder, caret, accent, from, via, to) must map to var(--<status>-bg|text|border) or var(--color-accent-<shade>). This is the general net that complements the context-specific R3/R7/R13.",
      "violation": "className contains a saturated Tailwind color utility, e.g. text-blue-600, bg-emerald-500, border-rose-400, ring-indigo-500.",
      "autoFixable": true,
      "severity": "error"
    },
    {
      "id": "R15-no-neutral-hardcoded-colors",
      "description": "Neutral colors SHOULD use theme surface tokens. Tailwind neutral families (slate, gray, zinc, neutral, stone) plus white/black are tokenised only for SURFACES (var(--bg-app|card|muted)) — bg-* utilities are auto-fixed. Neutral text/border have NO token in the current theme: they are reported for manual review (tokenise them in scaffold-theme, or accept them as idiomatic neutrals).",
      "violation": "className contains a neutral Tailwind color utility, e.g. text-gray-500, bg-slate-100, border-zinc-200, bg-white, text-black.",
      "autoFixable": true,
      "severity": "warning"
    },
    {
      "id": "R16-token-role-misuse",
      "description": "Status tokens carry fixed roles: --<status>-bg is a PALE surface, --<status>-text is a SATURATED foreground, --<status>-border is a mid tint, --<status>-dot is a saturated indicator. Each must be used in its role. A -text token used as a background (bg-[var(--<status>-text)]) is illegible — content disappears, especially in dark mode; a -bg token used as text (text-[var(--<status>-bg)]) is invisible. The fix flips the misused token back to its role. When the element is a badge or banner, complete the soft triplet bg-[var(--<status>-bg)] text-[var(--<status>-text)] border-[var(--<status>-border)] manually. This catches a class of bug that R14 cannot — the colour is already a var(--token), so it is neither a hex (R2) nor a Tailwind family (R14): the token is simply bound to the wrong utility.",
      "violation": "A status token is bound to the wrong utility prefix: bg-[var(--<status>-text)] (saturated text colour used as a fill) or text-[var(--<status>-bg)] (pale surface colour used as text).",
      "autoFixable": true,
      "severity": "error"
    },
    {
      "id": "R17-status-token-as-decoration",
      "description": "Status tokens (--success, --warning, --error, --info) carry FIXED MEANING — ok / caution / bad / neutral-info. They must signal real status, never serve as a decorative or categorical palette. Misuse is semantic, not mechanical: the value is already a var(--token) bound to the correct utility, so R2 (hex), R14 (Tailwind family) and R16 (utility role) all stay silent. The tell-tale is a color map whose decorative keys (blue, green, yellow, purple, …) are aliased to status tokens — e.g. const colorClasses = { blue: 'bg-[var(--info-bg)]', green: 'bg-[var(--success-bg)]', yellow: 'bg-[var(--warning-bg)]' } used to colour neutral metric/stat cards. A count of tenants rendered with --warning reads as an alert; a count rendered with --success implies success. Fix by reserving status tokens for real status (an active/inactive badge, a validation banner) and colouring decorative categories with var(--color-accent-<shade>) or a neutral surface (var(--bg-secondary)) instead. Not auto-fixable: choosing the right decorative token per category is a semantic decision the engine cannot make. Decorative keys mapped to var(--color-accent-*) or var(--bg-*) are correct and NOT flagged.",
      "violation": "A decorative/categorical color-name key (blue, green, yellow, purple, …) is mapped to a status token value containing var(--<success|warning|error|info>-…), i.e. a status token used purely for its hue on non-status content.",
      "autoFixable": false,
      "severity": "warning"
    },
    {
      "id": "R18-colors-outside-classname",
      "description": "Tailwind color utilities appearing in JS data structures (string literals, object values, array elements) outside className attributes indicate hardcoded colors that escape the className-scoped R14/R15 scanners. These typically appear in config objects, option arrays, or conditional maps like { selectedBorder: 'border-gray-500' } and render with a hardcoded color even though no className violation fires.",
      "violation": "A string literal outside any className attribute contains a Tailwind color utility (bg-blue-500, text-red-300, border-gray-200, etc.).",
      "autoFixable": false,
      "severity": "warning"
    },
    {
      "id": "R19-hex-in-inline-style",
      "description": "Hex colors (#xxx, #xxxxxx), rgb(), or rgba() values in inline style={{ }} props bypass the design token system entirely. These colors cannot respond to theme changes or dark mode toggling. Use CSS variable tokens via className (bg-[var(--token)]) or import the token value from the theme for dynamic styling.",
      "violation": "An inline style={{ }} prop contains a hex color (#xxx/#xxxxxx), rgb() or rgba() value bound to a color CSS property (backgroundColor, color, borderColor, fill, stroke).",
      "autoFixable": false,
      "severity": "warning"
    },
    {
      "id": "R20-accent-overuse",
      "description": "Accent tokens used on non-CTA elements (cards, sections, headers) create visual noise — every surface competes for attention and nothing reads as the primary action. Patterns: (A) thick accent border (border-2+) on a card/container, (B) pale accent-50/100 as section/header background instead of var(--bg-secondary), (C) full accent fill (shade >= 500) on a non-button element. Reserve full accent for buttons/CTAs.",
      "violation": "An accent token (var(--color-accent-*)) is used in a visually heavy way on a non-button element: thick border, section background, or full fill.",
      "autoFixable": false,
      "severity": "warning"
    },
    {
      "id": "R21-cta-inline-bypass",
      "description": "Primary CTA buttons MUST use the .btn .btn-primary helper class. The platform theme owns the accent fill, hover, text colour, radius and padding — re-assembling them inline (bg-[var(--color-accent-500|600)] text-white hover:… rounded-[var(--radius-button)]) drifts between pages, skips dark handling and bypasses the single source of truth. Project-aware: only fires when the project ships .btn-primary (the platform baseline does). Auto-fix prepends `btn btn-primary` and strips the helper-owned colour utilities; the apply-diff review tidies any residual spacing. Complements R20 (the same fill on a NON-button element is overuse).",
      "violation": "A <button> carries a full accent fill (bg-[var(--color-accent-500)] or -600), typically with text-white, but does NOT use the `btn` helper class.",
      "autoFixable": true,
      "severity": "error"
    },
    {
      "id": "R22-soft-accent-bypass",
      "description": "Soft / pale accent surfaces MUST use the named soft-accent primitive — the .badge-accent helper or the --accent-bg/--accent-text/--accent-border triplet — not an ad-hoc opacity tint (bg-[var(--color-accent-500)]/10). Arbitrary opacities drift between components and do not dark-adapt. Project-aware: only fires when the project provides the soft-accent primitive (.badge-accent / --accent-bg). Distinct from R20 (which flags accent-50/100 used as a section background).",
      "violation": "An accent token is opacity-modified in className (bg-/text-/border-[var(--color-accent-<shade>)]/<opacity>) instead of using .badge-accent or the soft-accent triplet.",
      "autoFixable": false,
      "severity": "warning"
    },
    {
      "id": "R24-dangling-token-ref",
      "description": "A var(--token) referenced in className MUST be defined in the project's theme. A reference to an undefined token (e.g. --bg-primary / --border-primary where the theme defines --bg-card / --border-color) resolves to nothing or an inherited value and renders inconsistently from page to page — defeating UI harmonisation. Project-aware and SAFE: only runs when the project's own CSS defines the core tokens (the engine's `tokensComplete` gate, so the scanned token set is authoritative) and skips package-owned families it cannot see (Shoelace --sl-*, Tailwind --tw-*). Only a bare var(--x) (no fallback) is flagged. Not auto-fixable — choosing the correct replacement token is a semantic decision.",
      "violation": "className contains a bare var(--x) whose custom property is not defined anywhere in the project's theme CSS (and is not a known package-owned --sl-*/--tw-* token).",
      "autoFixable": false,
      "severity": "warning"
    },
    {
      "id": "R25-content-width",
      "description": "Page content width is owned by the layout (<PageTemplate width=\"standard\"|\"focused\">), never hand-rolled on the page. Only TWO widths are sanctioned: 'standard' = full-width fluid (lists/tables/dashboards/config); 'focused' = a centered column capped at --content-max-narrow (forms/wizards/empty-states). Both keep a --content-gutter-x so content clears the edge chrome (doc launcher, sidebar handle). Ad-hoc page widths (Tailwind `container mx-auto`, a page-root `max-w-2xl..7xl mx-auto`, fractional w-3/4 etc.) make modules render at different measures and break UI harmony — banned. SAFE & project-aware: only on page files, only when the project ships PageTemplate, and modal/overlay containers (fixed / inset-0) are skipped (their max-w is legitimate). Not auto-fixable — moving width to the layout prop is a structural edit.",
      "violation": "A page file hard-codes its content width: a className with `container` + `mx-auto`, or a page-root `max-w-{2xl..7xl|screen-*}` + `mx-auto` outside a modal/overlay, instead of <PageTemplate width=\"focused\">.",
      "autoFixable": false,
      "severity": "warning"
    },
    {
      "id": "R26-native-form-control",
      "description": "Modern form controls (datepicker, dropdown, multi-select) must be the theme-compliant primitives from scaffold-ui-primitives — DateInput / EnumSelect / MultiSelect — not native HTML controls. Native <input type=\"date|datetime-local\"> and <select> render browser-chrome widgets (calendar popup, option list) that ignore the design system: corners (--radius-input), surface (--bg-card), borders, focus ring and dark mode all differ per OS/browser. SAFE & project-aware (like R1/R21/R22): only on page files, and each control is flagged ONLY when the matching primitive exists in the project (src/components/ui/DateInput|EnumSelect|MultiSelect) — never demands a component the app lacks. Report-only: the swap changes props (options[], ISO value, string[]), so it is not a mechanical auto-fix. R23 stays reserved for segmented-control anatomy.",
      "violation": "A page file renders a native <input type=\"date\"|\"datetime-local\"> (use <DateInput>), a native <select> (use <EnumSelect>) or <select multiple> (use <MultiSelect>) while the project ships the matching theme-compliant primitive.",
      "autoFixable": false,
      "severity": "warning"
    },
    {
      "id": "R27-responsive-datatable",
      "description": "List pages should render <ResponsiveDataTable> (from @/components/ui/ResponsiveDataTable) rather than a bare <DataTable>. The wrapper makes the table responsive — the default columns (code, label, actions) stay visible at every width while the remaining columns appear progressively as the viewport widens (minBreakpoint per column) — and it clips overflowing cell values with an ellipsis + a tooltip showing the full text. SAFE & gated: only on *ListPage.tsx files; a page already using <ResponsiveDataTable> (or with no DataTable at all) is never flagged. Anti-gate: WARNING only — a bare <DataTable> still renders, this nudges toward the responsive baseline. Not auto-fixable here — re-run scaffold-component (and scaffold-ui-primitives to add the wrapper).",
      "violation": "A *ListPage.tsx renders a bare <DataTable> without wrapping it in <ResponsiveDataTable>, so its columns do not adapt to the available width and long values are not truncated with a tooltip.",
      "autoFixable": false,
      "severity": "warning"
    },
    {
      "id": "R28-dataviz-hardcoded-colors",
      "description": "Dashboard / chart colours MUST come from the theme, never from the file. This is the one colour blind spot every other rule shares: chart colours reach Recharts as real colour strings through JSX props (fill=\"#3b82f6\", stroke={COLORS[i]}) or a palette array — never through className — so R2 (hex in className), R14/R15 (Tailwind families), R18 (utilities in literals) and R19 (hex in style={{}}) all stay silent while the chart renders off-brand and ignores dark mode. The sanctioned channel: categorical series read the palette through useDatavizPalette() (a hook, because Recharts cannot take var() in an SVG fill), which resolves the PLATFORM theme first — --dataviz-cat-1..12, written live on <html> by the @atlashub/smartstack theme runtime (ThemeContext.applyDataViz) from the tenant's UI configuration — then the scaffolded --dataviz-1..8 (themselves emitted as var(--dataviz-cat-N, <accent-derived>)), then the accent ramp; chart chrome uses --chart-grid/-axis/-tooltip-bg/-tooltip-text (each resolving through the live theme tokens), KPI figures use --kpi-value/-label/-trend-up/-trend-down, and project-specific category colours are declared ONCE via scaffold-theme (theme.dataviz: [...]). Gated to chart code (imports recharts, *DashboardPage.tsx, or src/components/dashboard/**) so ordinary pages are untouched; ChartCard.tsx is exempt from the raw-recharts shape (it IS the wrapper) and useDatavizPalette.ts entirely (its last-resort constant is the documented SSR fallback). Three shapes: (A) colour literal in chart code — error; (B) recharts imported outside ChartCard — warning, it opens a second untokenised chart path; (C) project-aware — the stylesheets define neither --dataviz-1 nor --dataviz-cat-1 — warning only, never error: the platform runtime injects --dataviz-cat-* as inline styles that no static CSS scan can see. Report-only: mapping a literal onto the right token is a design decision.",
      "violation": "Chart code hardcodes a colour (#hex / rgb() / hsl()) instead of the --dataviz-*/--chart-*/--kpi-* tokens, imports recharts outside ChartCard.tsx, or the project's stylesheets carry no categorical palette (--dataviz-1..8 / --dataviz-cat-1..12).",
      "autoFixable": false,
      "severity": "error"
    },
    {
      "id": "R29-modal-centering",
      "description": "Every modal overlay (a className carrying `fixed` + `inset-0`) MUST be the canonical centred wrapper: `fixed inset-0 z-[100] flex items-center justify-center p-4 bg-[var(--overlay-scrim)] backdrop-blur-sm` (cf. ConfirmModal). The platform z-scale is header z-[70], drawer z-[90], modals z-[100] — below 100 a dialog slides UNDER the app header (or under the drawer that opened it); without `items-center` it sticks to the top of the viewport; a hardcoded `bg-black/NN` scrim ignores the theme's --overlay-scrim (0.6 light / 0.7 dark). NO heuristic separates a sidebar scrim from a modal — every non-conforming overlay is a violation unless it carries a reasoned exemption marker `modal-overlay-exempt: <reason>` on the className line or the line above (sidebar scrims, bottom sheets, command palettes, drawers, fullscreen viewers are the legitimate exemptions). Unlike the page-scoped rules, R29 also scans src/components/** and src/layouts/** — that is where modals actually live. Auto-fix is mechanical and idempotent: items-start→items-center (only when flex+justify-center are present and the overlay has no overflow-y-auto — the unreachable-modal-top trap), z<100→z-[100], bg-black/40|50|60→bg-[var(--overlay-scrim)], strip of the <dialog>-reset residue m-0 max-w-none max-h-none border-none. Structural cases (no flex at all, translate(-50%,-50%) panels, legacy inline-block align-bottom markup, non-literal className) stay errors to rewrite by hand toward the canonical wrapper — put max-h + overflow-y-auto on the CARD when it can outgrow the viewport, never items-start on the overlay.",
      "violation": "A className with `fixed inset-0` is not a centred modal (missing flex items-center justify-center), sits below z-[100] (under the app header z-[70]), hardcodes a bg-black/NN scrim instead of bg-[var(--overlay-scrim)], keeps <dialog>-reset residue, or is a non-literal className the scanner cannot audit — and carries no `modal-overlay-exempt: <reason>` marker.",
      "autoFixable": true,
      "severity": "error"
    },
    {
      "id": "R30-elevation-tokens",
      "severity": "warning",
      "autoFixable": true,
      "description": "Shadows come from the theme's elevation scale, never from raw Tailwind shadow classes. scaffold-theme emits --shadow-card (resting surfaces: cards, small affordances) and --shadow-overlay (floating surfaces: dropdowns, popovers, dialogs), redefined for dark mode where light-mode shadows vanish. A raw shadow-sm/md/lg/xl/2xl escapes that scale: the surface keeps a hardcoded elevation the theme cannot soften or flatten. Fix: shadow-sm/md -> shadow-[var(--shadow-card)], shadow-lg/xl/2xl -> shadow-[var(--shadow-overlay)]."
    }
  ],
  "importMap": {
    "PageTemplate": "@/components/ui/PageTemplate",
    "DataTable": "@/components/ui/DataTable",
    "ResponsiveDataTable": "@/components/ui/ResponsiveDataTable",
    "TruncatedText": "@/components/ui/TruncatedText",
    "Badge": "@/components/ui/Badge",
    "Skeleton": "@/components/ui/Skeleton",
    "EmptyState": "@/components/ui/EmptyState",
    "StatCard": "@/components/ui/StatCard",
    "EntityCard": "@/components/ui/EntityCard",
    "ConfirmModal": "@/components/ui/ConfirmModal",
    "Breadcrumb": "@/components/ui/Breadcrumb",
    "Drawer": "@/components/ui/Drawer",
    "Tooltip": "@/components/ui/Tooltip",
    "icons": "lucide-react",
    "i18n": "react-i18next",
    "router": "react-router-dom",
    "extensions": "@atlashub/smartstack",
    "types": "@atlashub/smartstack"
  }
}
