Use this skill when auditing existing code for Gamut usage and you need a consolidated report — checks dependencies, setup, import patterns, styled() wrapping that bypasses system props, hardcoded colors, bespoke component duplication, and test setup, with pointers to remediation skills.
Installs into .claude/skills of the current project.
Are you the author of Gamut Review?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/codecademy-gamut-review)
---
name: gamut-review
description: Use this skill when auditing existing code for Gamut usage and you need a consolidated report — checks dependencies, setup, import patterns, styled() wrapping that bypasses system props, hardcoded colors, bespoke component duplication, and test setup, with pointers to remediation skills.
---
# Gamut Review
Audit existing code at the path the user provides (default: current working directory). Find violations and misuse; do not generate new code.
When `DESIGN.md` is present at the audit root, use it as the authoritative reference for product design intent, token names, and component patterns. It is copied from `DESIGN.Codecademy.md`, `DESIGN.Percipio.md`, or `DESIGN.LXStudio.md` in `@codecademy/gamut` agent-tools (via `gamut plugin install --theme <name>`). When a finding maps to a skill, note it in the report so the developer knows where to get remediation guidance.
Run Check 0 first, then Checks 1–6, then print a single consolidated report using the format at the end of this file.
Remediation skills: [`gamut-theming`](../gamut-theming/SKILL.md) · [`gamut-color-mode`](../gamut-color-mode/SKILL.md) · [`gamut-system-props`](../gamut-system-props/SKILL.md) · [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md) · [`gamut-typography`](../gamut-typography/SKILL.md) · [`gamut-testing`](../gamut-testing/SKILL.md) · [`gamut-z-index`](../gamut-z-index/SKILL.md) · [`gamut-component-first`](../gamut-component-first/SKILL.md)
---
## Check 0 — DESIGN.md present
Resolve the audit root from the user's path if provided, otherwise the current working directory. Look for `DESIGN.md` at that root (not inside `node_modules` or package subfolders unless the audit path is explicitly that folder).
| Result | Action |
| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Found | Report `✓ DESIGN.md present (<path>)`. Proceed with Checks 1–5 using this file for product/theme context. |
| Missing | Report `✗ DESIGN.md not found` as a blocking finding. Include remediation: from the repo root run `gamut plugin install cursor --theme core` (or `percipio`, `lxstudio`, `admin`, `platform`; also `claude`), or manually copy the matching `DESIGN.*.md` from `@codecademy/gamut` agent-tools and rename to `DESIGN.md`. Still run Checks 1–3 and 5. For Check 4, list hex violations with `palette:` / `semantic:` only where Appendix A/B apply without product YAML — prefix the Hardcoded colors section with `⚠ low confidence — no DESIGN.md` and do not assume Codecademy Core semantics; do not use Appendix B shortcuts as authoritative. |
---
## Check 1 — Dependencies
Read `package.json` at the audit root. Inspect `dependencies`, `devDependencies`, and `peerDependencies` combined.
| Package | Expectation |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@codecademy/gamut` | Required — core component library |
| `@codecademy/gamut-styles` | Recommended — design tokens and theme primitives |
| `@codecademy/variance` | Recommended — style-prop system used by Gamut internals |
| `@codecademy/gamut-kit` | Acceptable alternative meta-package — re-exports `@codecademy/gamut`, `@codecademy/gamut-styles`, `@codecademy/variance`, and more. Treat its presence as satisfying the three rows above; do not separately flag those packages as missing. **Caveat:** requires npm or yarn with `nodeLinker: node-modules`; not compatible with yarn Plug'n'Play. Flag as ⚠ warning if `.yarnrc.yml` shows `nodeLinker: pnp`. |
---
## Check 2 — Setup
First detect whether the project uses TypeScript by checking for `tsconfig.json` at the audit root or `typescript` in `package.json` `dependencies`/`devDependencies`. Use this to set severity for the Theme augmentation row below.
Search source files (`.ts`, `.tsx`, `.js`, `.jsx`) for these symbols. Skip `node_modules`, `dist`, `.next`, `build`, `.turbo`.
| Symbol | Expectation |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GamutProvider` | Required — must appear at least once (app root wrapper) |
| `ColorMode` | Recommended — enables semantic light/dark theming |
| `Background` | Recommended — semantic surface color via `@codecademy/gamut-styles` |
| `declare module '@emotion/react'` | **Required if TypeScript** — `Theme` must be augmented with the active theme type (e.g. `CoreTheme`) so scale props type-check correctly; grep `.d.ts` and `.ts`/`.tsx` source files for this string. **Recommended if not TypeScript** — note that adopting TypeScript is recommended and that `theme.d.ts` will be needed when it is. |
For each found symbol report the first file path where it appears.
**Conditional — `StyleProps` with `states()`/`variant()`**: If source files contain `states(` or `variant(` from `@codecademy/gamut-styles`, check whether component prop interfaces use `StyleProps<typeof ...>` from `@codecademy/variance`. When `states()`/`variant()` are present but `StyleProps` is absent from associated component interfaces, report as ⚠ warning: `StyleProps not used — state/variant props may be untyped`. Remediation: `import { StyleProps } from '@codecademy/variance'` and add `extends StyleProps<typeof myStates>` to the component interface. Skill reference: [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md).
---
## Check 3 — Import patterns
Grep source files for any of these patterns. Each match is an error.
| Pattern | Reason |
| ------------------------------- | ----------------------------------------------------------------------- |
| `@codecademy/gamut/dist/` | Deep dist import — bypasses public API and breaks on internal refactors |
| `@codecademy/gamut/src/` | Deep src import — not part of the published package |
| `@codecademy/gamut-styles/src/` | Deep src import — use the package root |
| `@codecademy/variance/src/` | Deep src import — use the package root |
Report each violation as `file:line`.
---
## Check 3b — SCSS/CSS module imports, className, and inline styles on Gamut components
Gamut components are styled via the variance system (system props, `css()`, `variant()`, `states()` from `@codecademy/gamut-styles`). Importing SCSS/CSS modules, passing `className`, and passing an inline `style` prop to Gamut components all bypass this system, break ColorMode token propagation, and prevent system props from composing correctly.
**Step 1 — SCSS/CSS module imports**
Grep source files (`.ts`, `.tsx`, `.js`, `.jsx`) for:
```
import .* from '.*\.(scss|css)'
```
Skip `node_modules`, `dist`. Each match is an error unless:
- The import targets a third-party stylesheet (e.g. a carousel or date-picker vendor sheet that cannot be replaced) — flag as ⚠ warning with note "third-party vendor styles".
- The file is a global reset or application shell (not a component) — flag as ⚠ warning.
Report the count and list of files. If there are more than 5 files, group by directory and report totals rather than listing every file.
**Step 2 — className on Gamut components**
Grep source files for `className=` appearing on any of the core Gamut component names in the same JSX element opening tag. The known Gamut components to check:
```
Box, FlexBox, Column, LayoutGrid, GridBox, Card, Text, Anchor,
FillButton, StrokeButton, TextButton, CTAButton, IconButton, Toggle,
List, ListRow, ListCol, Background, Disclosure
```
Pattern (grep, case-sensitive):
```
<(Box|FlexBox|Column|LayoutGrid|GridBox|Card|Text|Anchor|FillButton|StrokeButton|TextButton|CTAButton|IconButton|Toggle|List|ListRow|ListCol|Background|Disclosure)\b[^>]*\bclassName=
```
Each match is an error. Report as `file:line <ComponentName className={...}>`.
Severity note: `className` is not always forbidden — some Gamut components accept it for integration with third-party tools (e.g. passing a class to an external drag-and-drop library). Downgrade to ⚠ warning only when the usage is clearly an integration seam, not styling.
**Step 3 — inline `style` prop**
Grep source files (`.ts`, `.tsx`, `.js`, `.jsx`) for an inline `style` prop on any JSX element, and specifically on the known Gamut component names from Step 2:
```
style=\{\{
```
and
```
<(Box|FlexBox|Column|LayoutGrid|GridBox|Card|Text|Anchor|FillButton|StrokeButton|TextButton|CTAButton|IconButton|Toggle|List|ListRow|ListCol|Background|Disclosure)\b[^>]*\bstyle=\{
```
Skip `node_modules`, `dist`. Each match is an error — an inline `style` object silently bypasses ColorMode and the variance system exactly like `className` does, and its values are almost always hardcoded hex or pixel literals rather than tokens.
Before reporting a match, check the matched line and the line immediately above it for an `eslint-disable` / `eslint-disable-next-line` comment referencing a style-related rule (e.g. `react/forbid-component-props`, `react/forbid-dom-props`, or any bare `eslint-disable(-next-line)` directly above the match). If found, still report the match but annotate it `(eslint-disabled)` and downgrade to ⚠ warning — someone made a deliberate, reviewed exception, but the suppression may be stale or broader than intended, so surface it rather than silently skip it.
Report as `file:line <ComponentName style={{...}}>` (or the bare JSX tag when not a known Gamut component).
Severity note: like `className`, downgrade to ⚠ warning only for a clear third-party integration seam (e.g. a style object required by an external widget's API) — not for layout or color values that a Gamut component or system prop could express.
Remediation: replace SCSS module rules and inline `style` objects with system props directly on the Gamut component — use semantic ColorMode tokens as values (`color="text"`, `bg="background"`, `borderColor="border-primary"`, etc.) rather than hardcoded hex, pixel literals, or inline objects; use `css()`, `variant()`, or `states()` from `@codecademy/gamut-styles` (with `styled` from `@emotion/styled`) for styles not expressible as system props; delete the SCSS file when all rules are migrated.
Skill references: [`gamut-system-props`](../gamut-system-props/SKILL.md) · [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md) · [`gamut-color-mode`](../gamut-color-mode/SKILL.md)
---
## Check 3c — Nested selectors
Nested selectors inside styled-component or Emotion template literals cause hard-to-isolate side effects and make consistent updates difficult. The [Gamut Best Practices](https://gamut.codecademy.com/?path=/docs-meta-best-practices--page) page flags two kinds as "at your own risk": tag selectors and Gamut component selectors.
**Step 1 — Tag selectors**
Grep source files (`.ts`, `.tsx`, `.js`, `.jsx`) for bare HTML tag names appearing as CSS selector lines inside styled-component or Emotion template literals. Skip `node_modules`, `dist`, `.next`, `build`, `.turbo`.
- Pattern A (named tags):
```
^\s*(div|span|p|ul|li|ol|a|img|h[1-6]|table|thead|tbody|tr|td|th|form|section|header|footer|nav|main)\s*\{
```
- Pattern B (universal selector):
```
^\s*\*\s*\{
```
False-positive risk: `*` appears in JSDoc comment bodies (` * {`). Post-filter matches where the line is a comment (starts with `//` or matches `\s*\*\s`). For remaining matches, verify context before marking as a violation.
Do NOT include SVG primitive tag names (`path`, `rect`, `circle`, `line`, `polyline`, `svg`) — styled SVG primitives in icon and form components are normal and not the target of this rule.
Exemptions (downgrade to `ℹ note`, not warning):
- Files that import `Global` from `@emotion/react` — intentional global reset/injection stylesheets.
- Files whose name matches `*reboot*`, `*reset*`, `*global*`, or `*base-styles*`.
**Step 2 — Gamut component selectors**
Rather than enumerating every Gamut component by name (brittle, misses new additions), scope to files that already import from `@codecademy/gamut`, then grep those files for any PascalCase identifier used as a CSS selector:
1. Find files that contain `from '@codecademy/gamut'`.
2. In those files, grep for:
```
\$\{[A-Z][A-Za-z]+\}[^{]*\{
```
This matches any `${PascalCaseName}` followed by a rule block — i.e., a component used as a CSS child selector.
Each match means a component is being targeted from a parent styled wrapper rather than styled directly. Report as `file:line ${ComponentName} { ... }`.
Severity note: `&:pseudo ${ComponentName}` (pseudo-class combinator preceding the interpolation) is lower risk — downgrade those to ⚠ warning with a note to verify scope. Bare `${ComponentName} { }` selector blocks are the primary target.
**Severity:** ⚠ warning for all matches (per Best Practices: "you may still do so, but at your own risk").
**Remediation:**
_Tag selectors_ — plain HTML elements do not need to become Gamut components. Two valid paths:
- Use `Box`, `FlexBox`, or `GridBox` with the `as` prop to render as the intended element — no extra DOM node needed: `<Box as="section" p={16} color="text">`, `<FlexBox as="nav" gap={8}>`.
- Style in place with `styled.div(css({ color: 'text', p: 16 }))` using semantic ColorMode tokens from `@codecademy/gamut-styles` — keeps the element but brings it into the design system token graph.
Replace the parent's nested selector rule with one of the above and remove the selector block.
_Gamut component selectors_ — pass system props directly to the component (`alignSelf`, `mt`, etc.) rather than targeting it from a parent wrapper. Where dynamic behavior spans multiple children, prefer `css()` with `variant()` or `states()` from `@codecademy/gamut-styles` keyed to data attributes or boolean props on the parent.
Skill references: [`gamut-system-props`](../gamut-system-props/SKILL.md) · [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md)
---
## Check 3d — `styled(GamutComponent)` bypassing system props
Wrapping an already-Gamut component in `styled()` and writing raw CSS (a tagged template or plain object, not `css()`/`variant()`/`states()`) is the same bypass as `className`/inline `style` on a Gamut component wearing a different hat — none of it gets ColorMode token resolution, responsive-prop scaling, or the variance pipeline, and it duplicates an API the wrapped component already exposes as props.
**Step 1 — scope to files that already import from `@codecademy/gamut`**
Same technique as Check 3c Step 2 (enumerating every component name by hand is brittle and misses new additions). Find files containing:
```
from '@codecademy/gamut'
```
**Step 2 — grep those files for `styled(PascalCaseName)` not wrapped in `css()`/`variant()`/`states()`**
Two patterns, both violations:
```
styled\([A-Z][A-Za-z]+\)`
```
```
styled\([A-Z][A-Za-z]+\)\(\s*\{
```
The first catches the tagged-template form (`` styled(Box)`display: flex;` ``); the second catches the plain-object function-call form (`styled(Box)({ display: 'flex' })`). `styled(Box)(css({...}))`, `styled(Box)(variant({...}))`, and `styled(Box)(states({...}))` do **not** match either pattern — the character right after the opening `(` is a letter, not a backtick or `{`. That's the compliant form; don't flag it.
Confirm the matched name is actually the Gamut import in that file (not an unrelated same-named local component) before reporting.
**Step 3 — classify each match**
Read the properties inside the flagged block:
- **Every property has a direct system-prop equivalent** (layout/flex/space/color/border/positioning/typography — see [`gamut-system-props`](../gamut-system-props/SKILL.md) prop groups) → ✗ error. Remediation: delete the `styled()` wrapper; pass the same values as props directly on the JSX element. Flag `display: 'flex'`/`display:flex` specifically — recommend `FlexBox` instead of `Box` + a `display` prop. Note that `background` (unlike `bg`) has no token scale and takes any CSS value as-is — a gradient string is already valid as a plain `background` prop and does **not** by itself justify a `styled()` wrapper.
- **Something in the block isn't expressible as a prop** (`background-clip`, `background-blend-mode`, a variant that should branch on a prop, pseudo-selectors) → ⚠ warning. Remediation: keep `styled(ComponentName)`, but move the object into `css()`, `variant()`, or `states()` from `@codecademy/gamut-styles` instead of a raw literal — and pull the properties that _are_ expressible (padding, display, plain colors, gradients via `background`) back out to props rather than leaving them in the escape hatch just because one sibling property forced it.
**Step 4 — raw CSS property names inside `css()` silently skip the scale lookup**
Being wrapped in `css()` (the compliant form from Step 2) isn't enough on its own to guarantee token-scale treatment. `css()`'s scale-aware behavior only applies to the _alias_ keys variance defines (`p`, `m`, `bg`, `borderRadius`, `fontSize`, `fontWeight`, `lineHeight`, …) — not the literal CSS property name. `padding: 24` inside a `css({...})` call is **not** the same as `p: 24`: variance's static-CSS extraction (`getStaticCss` in `packages/variance/src/core.ts`) passes any key it doesn't recognize as an alias straight through as literal, unscaled CSS. A block that _looks_ correct because it's wrapped in `css()` can still bypass the spacing/typography scale entirely if the property inside is spelled out as the raw CSS name instead of its alias.
Grep for raw CSS property names as keys inside a `css({...})` call, where a shorter alias exists:
```
css\(\s*\{[^}]*\b(padding|margin|background-color|border-radius|font-size|font-weight|line-height)\s*:
```
(non-exhaustive — the pattern is: any CSS property name used as a key where its alias — `p`/`m`/`bg`/`borderRadius`/`fontSize`/`fontWeight`/`lineHeight` — belongs instead.)
Each match is **✗ error**, not a style nit — the value silently doesn't get the intended token/rem conversion regardless of what number is written, which is a functional bug, not a preference. This check applies independently of Step 3's classification — a block can pass Step 3 (genuinely needs `css()` for one property) and still fail Step 4 (a _different_ property inside that same block used the wrong key name).
**Step 5 — plain `<Box display="flex">`/`<Box display="grid">` (heuristic, not a bypass)**
Unlike Steps 1–4, this isn't a system-props bypass — `display` is a real system prop and the code works correctly. It's a semantic-consistency nit: `FlexBox`/`GridBox` already default to `display: flex`/`display: grid`, so reaching for `Box` + a `display` prop says the same thing more verbosely and loses the more legible component name at the call site.
Grep files already confirmed to import `Box` from `@codecademy/gamut` (Step 1) for:
```
<Box\b[^>]*\bdisplay=\{?["']flex["']\}?
<Box\b[^>]*\bdisplay=\{?["']grid["']\}?
```
Always **⚠ warning**, never ✗ — don't flag `display="inline-flex"`/`"inline-grid"`, or a conditionally-computed `display` (e.g. `display={isOpen ? 'flex' : 'none'}`); both are legitimate reasons to stay on `Box`.
Report as `file:line styled(ComponentName)` with the classification and the specific properties found, e.g.:
```
src/HeroSection.tsx:14 styled(Box)`...` — display, flex-direction, padding, color: white → delete wrapper, use FlexBox + props (color: use a semantic token)
src/ColumnTitle.tsx:8 styled(Box)`...` — background-clip: text + padding → wrap in css(), move padding to a prop
src/GlowShell.tsx:15 css({ padding: 24, ... }) — 'padding' is not a recognized alias; use 'p' or the value bypasses the spacing scale entirely (renders as an unscaled literal px value), even though the block correctly stays in css() for the gradient
src/Panel.tsx:9 <Box display="flex" ...> — same defaults as FlexBox, more legible as FlexBox
src/Grid.tsx:14 <Box display="grid" ...> — same defaults as GridBox, more legible as GridBox
```
Skill references: [`gamut-system-props`](../gamut-system-props/SKILL.md#dont-wrap-a-gamut-component-in-styled-to-hand-write-css) · [`gamut-style-utilities`](../gamut-style-utilities/SKILL.md)
---
## Check 4 — Hardcoded colors (semantic-first)
Rule: Inline hex literals in application UI code are violations. Remediation is not “replace hex with `navy-800`” — prefer semantic ColorMode tokens (`text`, `background`, `primary`, …) so light/dark and theme switches stay correct. Reserve raw palette tokens for colors that must stay fixed and for `bg` on `<Background>` from `@codecademy/gamut-styles` (section surfaces with content).
Align findings with project docs and Storybook:
- [`gamut-color-mode`](../gamut-color-mode/SKILL.md) skill — semantic alias tables and decision guide.
- [Foundations / ColorMode](https://gamut.codecademy.com/?path=/docs-foundations-colormode--page) — aliases per mode; `<Background>` behavior.
- [Meta / Best practices](https://gamut.codecademy.com/?path=/docs-meta-best-practices--page) — semantic colors + `css` / `variant` / `states` from `gamut-styles`.
- Foundations / Theme stories (Core, Admin, Platform, Percipio, LX Studio) — verify hex ↔ semantic if the product is not Codecademy Core.
Theme context: If Check 0 passed, infer product/theme from root `DESIGN.md` and `GamutProvider` / app config. If Check 0 failed, follow the low-confidence rules in Check 0 — do not assume Codecademy Core semantics. If `DESIGN.md` exists but theme is still unclear, add a report note to confirm against the correct theme Storybook page.
Discovery: Grep source files (`.ts`, `.tsx`, `.js`, `.jsx`, `.css`, `.scss`, `.less`) for inline hex literals (`#RGB` or `#RRGGBB`). Comparison is case-insensitive. Skip `node_modules`, `dist`, `.next`, `build`, `.turbo` (same spirit as other checks).
### Workflow (each hex match)
1. Context — Inspect the surrounding line(s): CSS property (`color`, `background`, `border-color`, …), JSX prop (`color`, `bg`, `borderColor`, SVG fill), or asset. Note whether the subtree is a section with content (candidate for `<Background>` + palette `bg`) vs component chrome (prefer semantics).
2. Identify palette — Normalize hex (case-insensitive); map to a Gamut palette name using Appendix A below. If missing from the appendix, match against `DESIGN.md` / `packages/gamut-styles` palette definitions.
3. Recommend semantic first — Use Appendix B (Core light literals) plus role:
- Body / UI foreground → `text`; strong emphasis → `text-accent`.
- Page or card fill → `background` / `background-primary` / state surfaces (`background-success`, `background-warning`, `background-error`).
- CTAs, links, hyper accents → `primary` (+ `primary-hover` on hover).
- Ghost / secondary buttons → `secondary`.
- Destructive → `danger` / `danger-hover`.
- Dividers / outlines → `border-primary` / `border-secondary` / `border-tertiary`.
- Inline feedback copy → `feedback-error` / `feedback-success` / `feedback-warning`.
- Disambiguation: `#FFD300` — warning copy → `feedback-warning`; yellow accent on top of primary-colored surfaces → `primary-inverse`.
- Same hex can map to multiple semantics (e.g. `#10162F` → `text` vs `border-primary` vs `secondary`): pick from property + component role.
4. When palette-only is OK — `bg` prop on `<Background>` (`<Background bg="hyper">`, etc.) is the primary place for fixed surface palette colors on sections. After replacing hex there, use a named palette token, not hex. Exceptions (flag with rationale): charts/data viz, third-party widgets, exported static illustrations — still prefer tokens over hex when feasible.
Severity: Hex on adaptive UI (random wrappers, `styled-components`, inline `style`) → error. Hex inside documented exceptions → warning with note.
Reporting: For each match outside token definition files:
`file:line 'HEX' → semantic: <token(s)> | palette: <token> | note: <theme/disambiguation>`
Use `semantic: (n/a)` only when no semantic applies (e.g. pure illustration); still give `palette: …`. If unmappable: `→ no Gamut token`.
Ignore hex inside design token definition files (e.g. `variables/colors.ts`, `_colors.scss`) — source of truth, not violations.
### Appendix B — Core light: hex → suggested semantic (shortcut)
Use with step 3; verify for non-Core themes.
| Hex (normalized) | Typical semantic direction |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `#10162f` | `text`, `border-primary`, or `secondary` (by role) |
| `#0a0d1c` | `text-accent` |
| `#ffffff` | `background` (fills), `secondary` (inverse ghost on dark — rare in light-only grep context) |
| `#fff0e5` | `background-primary` |
| `#f5ffe3` | `background-success` |
| `#fffae5` | `background-warning` |
| `#fbf1f0` | `background-error` |
| `#3a10e5` | `primary` |
| `#5533ff` | `primary-hover` |
| `#ffd300` | `feedback-warning` or `primary-inverse` (see disambiguation above) |
| `#cca900` | Often pairs with hover in dark mode; in light UI as literal hex → check palette appendix (`yellow-400`) then assign role |
| `#e91c11` | `danger` |
| `#be1809` | `danger-hover`, `feedback-error` |
| `#008a27` | `feedback-success` |
Hexes with no row above still get Appendix A palette id + role-based semantic guess (e.g. blue scale → often decorative or legacy marketing; prefer design review unless mapping clearly to `primary`).
### Appendix A — Hex → palette token (identification only)
Case-insensitive. Use to label `palette:` in the report; do not stop at this step without Appendix B / role triage.
| Hex | Token |
| --------- | -------------------------- |
| `#000000` | `black` |
| `#ffffff` | `white` |
| `#10162f` | `navy` / `navy-800` |
| `#0a0d1c` | `navy-900` |
| `#fff0e5` | `beige-100` |
| `#f5fcff` | `blue-0` |
| `#d3f2ff` | `blue-100` |
| `#66c4ff` | `blue-300` |
| `#3388ff` | `blue-400` |
| `#1557ff` | `blue-500` |
| `#1d2340` | `blue-800` |
| `#f5ffe3` | `green-0` |
| `#eafdc6` | `green-100` |
| `#aee938` | `green-400` / `lightGreen` |
| `#008a27` | `green-700` |
| `#151c07` | `green-900` |
| `#fffae5` | `yellow-0` |
| `#cca900` | `yellow-400` |
| `#ffd300` | `yellow-500` / `yellow` |
| `#211b00` | `yellow-900` |
| `#fff5ff` | `pink-0` |
| `#f966ff` | `pink-400` / `pink` |
| `#fbf1f0` | `red-0` |
| `#e85d7f` | `red-300` |
| `#dc5879` | `red-400` / `paleRed` |
| `#e91c11` | `red-500` / `red` |
| `#be1809` | `red-600` |
| `#280503` | `red-900` |
| `#ffe8cc` | `orange-100` |
| `#ff8c00` | `orange-500` / `orange` |
| `#5533ff` | `hyper-400` |
| `#3a10e5` | `hyper-500` / `hyper` |
| `#f5f5f5` | `gray-100` |
| `#eeeeee` | `gray-200` |
| `#e0e0e0` | `gray-300` |
| `#9e9e9e` | `gray-600` |
| `#616161` | `gray-800` |
| `#424242` | `gray-900` |
| `#fffbf8` | `beige-0` |
| `#8a7300` | `gold-800` / `gold` |
| `#d14900` | `orange-800` |
| `#ca00d1` | `pink-800` |
| `#006d82` | `teal-500` / `teal` |
| `#b3ccff` | `purple-300` / `purple` |
### Step 2 — Non-Gamut CSS custom properties (SCSS/CSS/Less files)
After the hex scan, grep `.scss`, `.css`, and `.less` files for `var(--` occurrences. For each custom property name found, classify it:
_Gamut-issued variables_ — skip these:
- `--color-*` (ColorMode semantic aliases)
- `--space*` (spacing scale)
- `--font*` (font-size scale)
- `--lineHeight*` (line-height scale)
- `--borderWidth*` (border-width tokens)
- `--fontFamily*` (font-family tokens)
- `--fontWeight*` (font-weight tokens)
_Non-Gamut variables_ — flag these:
Any other name — especially camel-cased semantic names like `--darkNeutralColor`, `--whiteColor`, `--lightPrimaryColor`, `--colorNavy800`, `--borderGreyColor` — indicates a parallel token system (Skillsoft/Percipio globals, legacy design tokens, or ad-hoc project variables). These variables are NOT set by `GamutProvider`/`ColorMode` and will be undefined inside a Gamut-scoped tree unless the host shell also loads them.
Severity: ✗ error for color-semantic variables (invisible in tests/Storybook without the host stylesheet); ⚠ warning for spacing/sizing variables that duplicate Gamut scale tokens.
Reporting: count unique non-Gamut variable names and list the top offenders with frequency. Do not enumerate every call-site — just the variable names and usage counts. Suggest the nearest Gamut semantic alias where obvious (e.g. `--darkNeutralColor` → `--color-text`, `--whiteColor` → `--color-background`).
---
## Check 5 — Test setup
Grep test files (`**/__tests__/**/*.{ts,tsx}`, `**/*.test.{ts,tsx}`, `**/*.spec.{ts,tsx}`) for these patterns. Skip `node_modules`, `dist`.
| Pattern | Verdict | Reason |
| ----------------------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `jest.mock\(.*@codecademy/gamut` | Error | Manual mocking bypasses theme context and produces false-positive tests; prefer `setupRtl` from `@codecademy/gamut-tests` (or a harness + `setupRtl`); use raw `MockGamutProvider` + `render` only for rare one-offs or Storybook mocks |
| `jest.mock\(.*@codecademy/gamut-styles` | Error | Same issue as above — mocking gamut-styles breaks token resolution |
| `from '@codecademy/gamut-tests'` | Good — report count of files using it | Correct import for `setupRtl` and `MockGamutProvider` |
| `from 'component-test-setup'` (without gamut-tests) | Warning | Should import `setupRtl` from `@codecademy/gamut-tests`, not directly from `component-test-setup` — the gamut-tests wrapper adds `MockGamutProvider` automatically |
| `new GamutProvider` or `<GamutProvider` in test files | Warning | Prefer `setupRtl`; use `MockGamutProvider` (sets `useCache={false}`, `useGlobals={false}`) in harnesses or stories, not `GamutProvider` directly |
| `jest.mock\(.*[Gg]amut[Pp]rovider` | Warning | Mocking any file whose path contains `GamutProvider` (including project-internal wrappers) strips Emotion/theme context; prefer `setupRtl` from `@codecademy/gamut-tests` |
Skill reference for remediation: [`gamut-testing`](../gamut-testing/SKILL.md)
---
## Check 6 — Bespoke component duplication
Unlike Checks 1–5, this check is heuristic, not deterministic — every match needs a human glance before acting. **Every Check 6 finding is reported with `⚠`. There is no `✗` option for this check** — if a match feels like a clear-cut violation, that feeling is exactly the failure mode this rule exists to catch (a confident-looking ARIA-role/hand-rolled-listener match is still just a pattern match, not a certainty).
The goal: find custom-built UI that duplicates something `@codecademy/gamut` already provides — a hand-rolled modal, dropdown, tooltip, or focus trap living next to the library that already solves it. See [`gamut-component-first`](../gamut-component-first/SKILL.md) for the full decision table and the "signals" list this check is built from.
**Step 1 — Suspicious ARIA roles without a matching Gamut import**
Grep source files (`.ts`, `.tsx`, `.js`, `.jsx`) for hand-set roles:
```
role=["']?(dialog|menu|tooltip|listbox|alert)["']?
```
For each match, check whether the same file imports the corresponding component from `@codecademy/gamut` (`Modal`/`Dialog` for `dialog`, `Menu` for `menu`, `ToolTip`/`PreviewTip`/`InfoTip`/`Tip` for `tooltip`, `SelectDropdown` for `listbox`, `Alert` for `alert`). If the import is absent, report as `⚠ <file>:<line> role="..." with no matching Gamut import`.
**Step 2 — Component files named after a Gamut component that don't import it**
Look for source files whose filename (not import path) matches a known Gamut component name — `Modal`, `Dialog`, `Dropdown`, `Tooltip`, `Popover`, `Menu`, `Toast`, `Accordion`, `Tabs`, `Pagination`, `Avatar`, `Badge`, `Tag` — and check whether that file imports the matching name from `@codecademy/gamut`. A same-named local file that does _not_ import from the library is a strong signal of a parallel implementation. Report as `⚠ <file> filename matches a Gamut component; no @codecademy/gamut import found`.
**Step 3 — Hand-rolled dismiss/focus-trap logic**
Grep for manual Escape-key or outside-click dismiss handling that isn't going through Gamut's `Overlay`/`FocusTrap`/`PopoverContainer`:
```
(key === ['"]Escape['"]|addEventListener\(['"]keydown)
```
in files that don't import `Overlay`, `FocusTrap`, or `PopoverContainer` from `@codecademy/gamut`. Same idea for manual outside-click listeners (`addEventListener('click'` at the document/window level alongside a "contains" check). Report as `⚠ <file>:<line> hand-rolled dismiss logic, no FocusTrap/Overlay/PopoverContainer import`.
**Step 4 — SCSS/CSS module files named after a Gamut component**
Cross-reference with Check 3b's SCSS import list: a stylesheet named `Modal.scss`, `Dropdown.module.css`, `Tooltip.scss`, etc. is worth a second look even if Check 3b already flagged the import generically — the filename is the signal that this isn't just "some CSS," it's a parallel version of a specific Gamut component. Report as `⚠ <file> stylesheet named after a Gamut component`.
**Reporting:** for each match, name the likely Gamut component from the [decision table](../gamut-component-first/SKILL.md#decision-table-common-needs) and note this needs manual confirmation — a real product-specific one-off will look identical to a grep tool.
**Before finalizing the report**, re-scan every line under this section specifically for a `✗` icon. If you find one, that's a mistake — change it to `⚠`. **When computing the final `<N> error(s), <N> warning(s)` tally, count every Check 6 match toward the warning total, never the error total, even if a `✗` slipped through above** — this is the one place a stray icon can't corrupt the report's headline numbers.
Skill reference for remediation: [`gamut-component-first`](../gamut-component-first/SKILL.md)
---
## Output format
```
Gamut Review — <absolute path>
══════════════════════════════════════════════════
DESIGN.md
✓ present <path>/DESIGN.md
✗ missing run: gamut plugin install cursor --theme <core|percipio|lxstudio|…> [blocking for color audit]
Dependencies
✓ @codecademy/gamut <version>
⚠ @codecademy/gamut-styles not found — recommended
✗ @codecademy/variance not found — recommended
Setup
✓ GamutProvider found (src/App.tsx)
⚠ ColorMode not found — use ColorMode for light/dark theming [→ gamut-color-mode]
⚠ Background not found — use <Background> for semantic surfaces [→ gamut-color-mode]
✗ Theme augmentation not found — create src/theme.d.ts extending CoreTheme [→ gamut-theming]
(⚠ if TypeScript not detected: TypeScript not detected — recommended to adopt TypeScript; add src/theme.d.ts extending CoreTheme when you do [→ gamut-theming])
Import patterns
✓ Deep dist imports none found
✗ Deep src imports 2 occurrences
src/Thing.tsx:7
src/Other.tsx:12
SCSS modules, className & inline style [→ gamut-system-props] [→ gamut-style-utilities] [→ gamut-color-mode]
✗ SCSS/CSS imports 14 files — migrate to system props and css()/variant()
src/components/Card/Card.scss
src/components/Nav/Nav.scss (+ 12 more)
✗ className on Gamut components 9 occurrences
src/components/Card/Card.tsx:14 <Box className={styles.wrapper}>
src/components/Nav/Nav.tsx:7 <Text className={styles.title}>
✗ inline style on Gamut components 3 occurrences — use system props or css()/variant()/states() with semantic tokens
src/components/Hero/Hero.tsx:31 <Box style={{ color: '#10162F' }}>
src/components/Nav/Nav.tsx:19 <Text style={{ marginTop: 8 }}> (eslint-disabled) ⚠
Nested selectors [→ gamut-system-props] [→ gamut-style-utilities]
⚠ Tag selectors 3 occurrences — replace with system props or layout components (FlexBox, GridBox)
src/components/Nav/Nav.tsx:18 div { ... }
src/components/Hero/Hero.tsx:9 * { ... } (verify scope — may be JSDoc false positive)
⚠ Gamut component selectors 1 occurrence — use system props directly instead
src/components/Layout/Layout.tsx:12 ${Box} { align-self: start; }
(or: ✓ none found)
styled(GamutComponent) bypassing system props [→ gamut-system-props] [→ gamut-style-utilities]
✗ styled(Box) raw CSS 1 occurrence — every property has a prop equivalent
src/HeroSection.tsx:14 display, flex-direction, padding, color: white → delete wrapper, use FlexBox + props
⚠ styled(Box) raw CSS 1 occurrence — partially expressible, needs css() not a raw literal
src/ColumnTitle.tsx:8 background-clip: text + padding → wrap in css(), move padding to a prop
⚠ Box used where FlexBox/GridBox fits 2 occurrences — same defaults, more legible name
src/Panel.tsx:9 <Box display="flex" ...> → FlexBox
src/Grid.tsx:14 <Box display="grid" ...> → GridBox
(or: ✓ none found)
Hardcoded colors [→ gamut-color-mode]
✗ src/Card.tsx:22 '#10162F' → semantic: text | palette: navy-800 | note: Core light body copy
⚠ src/Hero.tsx:14 '#1557FF' → semantic: primary (if link/CTA) | palette: blue-500 | note: no exact semantic; confirm theme
⚠ src/Nav.tsx:8 '#BADA55' → semantic: (n/a) | palette: — | note: no Gamut token
✗ Non-Gamut CSS vars --darkNeutralColor (8 uses), --whiteColor (5 uses) → --color-text, --color-background
Test setup [→ gamut-testing]
✓ @codecademy/gamut-tests used in 12 test files
✗ jest.mock(@codecademy/gamut) 2 occurrences — remove; prefer setupRtl (or harness + setupRtl)
src/components/Foo/__tests__/Foo.test.tsx:3
src/components/Bar/__tests__/Bar.test.tsx:5
⚠ direct component-test-setup import 1 occurrence — import from @codecademy/gamut-tests
src/components/Baz/__tests__/Baz.test.tsx:2
Bespoke component duplication (heuristic — confirm manually) [→ gamut-component-first]
⚠ src/components/ConfirmDialog/ConfirmDialog.tsx:9 role="dialog" with no Modal/Dialog import — likely reinventing gamut-modal
⚠ src/components/Dropdown/Dropdown.tsx filename matches a Gamut component; no @codecademy/gamut import found — compare against SelectDropdown
(or: ✓ none found)
══════════════════════════════════════════════════
<N> error(s), <N> warning(s) found. (or "All checks passed." if none)
```
Icons: `✓` = pass, `⚠` = warning (recommended, not required), `✗` = error (required).
`[→ skill-name]` annotations indicate which Gamut skill has remediation guidance for that category.
After printing the report, offer one sentence of prioritized next-step advice based on what was found, then ask the user whether they'd like you to fix the findings — do not apply any remediation unprompted.