> Part of the [figma-generate-library skill](../SKILL.md).

# Token Creation Reference

This document covers Phase 1: creating variable collections, modes, primitives, semantic aliases, scopes, code syntax, styles, and validation. All code is copy-paste ready for `use_figma`.

---

## 1. Collection Architecture

Choose the pattern that matches your token count and complexity:

### Simple Pattern (< 50 tokens)

One collection, 2 modes. Appropriate for small projects or brand kits.

```
Collection: "Tokens"    modes: ["Light", "Dark"]
  color/bg/primary → Light: #FFFFFF, Dark: #1A1A1A
  spacing/sm = 8
```

### Standard Pattern (50–200 tokens) — Recommended Starting Point

Separate primitives from semantics. The real-world reference is Figma's Simple Design System (SDS): 7 collections, 368 variables, light/dark modes on semantic colors, single-mode primitives.

```
Collection: "Primitives"    modes: ["Value"]       ← raw hex values, no modes
  blue/500 = #3B82F6
  gray/900 = #111827
  white/1000 = #FFFFFF

Collection: "Color"         modes: ["Light", "Dark"] ← aliases to Primitives
  color/bg/primary → Light: alias Primitives/white/1000, Dark: alias Primitives/gray/900
  color/text/primary → Light: alias Primitives/gray/900, Dark: alias Primitives/white/1000

Collection: "Spacing"       modes: ["Value"]
  spacing/xs = 4, spacing/sm = 8, spacing/md = 16, spacing/lg = 24, spacing/xl = 32

Collection: "Typography Primitives"  modes: ["Value"]
  family/sans = "Inter", scale/01 = 12, scale/02 = 14, scale/03 = 16, weight/regular = 400

Collection: "Typography"    modes: ["Value"]        ← aliases to Typography Primitives
  body/font-family → alias family/sans
  body/size-md → alias scale/03
```

### Advanced Pattern (200+ tokens) — M3 Model

Multiple semantic collections, 4–8 modes. Use when you need light/dark × contrast × brand or responsive breakpoints.

```
Collection: "M3"           modes: ["Light", "Dark", "Light High Contrast", "Dark High Contrast", ...]
Collection: "Typeface"     modes: ["Baseline", "Wireframe"]
Collection: "Typescale"    modes: ["Value"]  ← aliases into Typeface
Collection: "Shape"        modes: ["Value"]
```

Key insight from M3: ALL 196 semantic color variables live in a SINGLE collection with 8 modes. Switching a frame's mode once updates every color simultaneously.

---

## 2. Creating Collections + Modes

### Creating a Primitives Collection

```javascript
const RUN_ID = "ds-build-2024-001"; // use the same RUN_ID throughout the build

// Create the collection
const primColl = figma.variables.createVariableCollection("Primitives");

// Rename the default "Mode 1" to "Value"
primColl.renameMode(primColl.modes[0].modeId, "Value");
const valueMode = primColl.modes[0].modeId;

// Tag for idempotency
primColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
primColl.setSharedPluginData('dsb', 'key', 'collection/primitives');

return {
  collectionId: primColl.id,
  modeId: valueMode,
  name: primColl.name
};
```

### Creating a Semantic Color Collection with Light/Dark Modes

```javascript
const RUN_ID = "ds-build-2024-001";

const colorColl = figma.variables.createVariableCollection("Color");

// Rename default "Mode 1" to "Light"
colorColl.renameMode(colorColl.modes[0].modeId, "Light");
const lightModeId = colorColl.modes[0].modeId;

// Add "Dark" mode — requires Professional plan or higher
// Throws "in addMode: Limited to N modes only" on Starter plan
const darkModeId = colorColl.addMode("Dark");

colorColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
colorColl.setSharedPluginData('dsb', 'key', 'collection/color');

return {
  collectionId: colorColl.id,
  lightModeId,
  darkModeId
};
```

**Mode plan limits:** Starter = 1 mode, Professional = 4 modes, Organization/Enterprise = 40 modes. If `addMode` throws, the file is on a Starter plan — tell the user and ask how to proceed.

### Creating a Spacing Collection (single mode)

```javascript
const RUN_ID = "ds-build-2024-001";

const spacingColl = figma.variables.createVariableCollection("Spacing");
spacingColl.renameMode(spacingColl.modes[0].modeId, "Value");
const valueMode = spacingColl.modes[0].modeId;

spacingColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
spacingColl.setSharedPluginData('dsb', 'key', 'collection/spacing');

return {
  collectionId: spacingColl.id,
  modeId: valueMode
};
```

---

## 3. Creating All Variable Types

### hex → {r, g, b} Conversion Helper

Colors in the Figma Plugin API are 0–1 range, not 0–255. Embed this helper in any script that creates color variables:

```javascript
function hexToRgb(hex) {
  const clean = hex.replace('#', '');
  return {
    r: parseInt(clean.substring(0, 2), 16) / 255,
    g: parseInt(clean.substring(2, 4), 16) / 255,
    b: parseInt(clean.substring(4, 6), 16) / 255
  };
}

// With alpha channel (for semi-transparent primitives like Black/200 at 10%):
function hexToRgba(hex) {
  const clean = hex.replace('#', '');
  const hasAlpha = clean.length === 8;
  return {
    r: parseInt(clean.substring(0, 2), 16) / 255,
    g: parseInt(clean.substring(2, 4), 16) / 255,
    b: parseInt(clean.substring(4, 6), 16) / 255,
    a: hasAlpha ? parseInt(clean.substring(6, 8), 16) / 255 : 1
  };
}

// Usage:
// hexToRgb('#3B82F6')        → {r: 0.231, g: 0.510, b: 0.965}
// hexToRgb('#14AE5C')        → {r: 0.078, g: 0.682, b: 0.361}
// hexToRgba('#0c0c0d1a')     → {r: 0.047, g: 0.047, b: 0.051, a: 0.102}
```

### Creating Primitive Color Variables (Real SDS Data)

This creates a subset of the Simple Design System's `Color Primitives` collection (Blue family, from the Standard pattern used by real design systems):

```javascript
function hexToRgb(hex) {
  const c = hex.replace('#', '');
  return { r: parseInt(c.slice(0,2),16)/255, g: parseInt(c.slice(2,4),16)/255, b: parseInt(c.slice(4,6),16)/255 };
}

const RUN_ID = "ds-build-2024-001";

// Get the Primitives collection created in the previous step
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const primColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/primitives');
if (!primColl) throw new Error("Primitives collection not found — run collection creation first");
const valueMode = primColl.modes[0].modeId;

    // Define primitives — use real values from your codebase
    const primitiveColors = [
      // Blue scale
      { name: 'blue/100', hex: '#EFF6FF' },
      { name: 'blue/200', hex: '#DBEAFE' },
      { name: 'blue/300', hex: '#93C5FD' },
      { name: 'blue/400', hex: '#60A5FA' },
      { name: 'blue/500', hex: '#3B82F6' },
      { name: 'blue/600', hex: '#2563EB' },
      { name: 'blue/700', hex: '#1D4ED8' },
      { name: 'blue/800', hex: '#1E40AF' },
      { name: 'blue/900', hex: '#1E3A8A' },
      // Gray scale
      { name: 'gray/100', hex: '#F9FAFB' },
      { name: 'gray/200', hex: '#F3F4F6' },
      { name: 'gray/300', hex: '#D1D5DB' },
      { name: 'gray/400', hex: '#9CA3AF' },
      { name: 'gray/500', hex: '#6B7280' },
      { name: 'gray/600', hex: '#4B5563' },
      { name: 'gray/700', hex: '#374151' },
      { name: 'gray/800', hex: '#1F2937' },
      { name: 'gray/900', hex: '#111827' },
      // White / Black
      { name: 'white/1000', hex: '#FFFFFF' },
      { name: 'black/1000', hex: '#000000' },
    ];

    const created = [];
    for (const { name, hex } of primitiveColors) {
      const v = figma.variables.createVariable(name, primColl, 'COLOR');
      v.setValueForMode(valueMode, hexToRgb(hex));
      // Primitives: EMPTY scopes (hidden from all pickers — designers use semantics)
      v.scopes = [];
      // Code syntax from the actual CSS variable name
      v.setVariableCodeSyntax('WEB', `var(--color-${name.replace('/', '-')})`);
      v.setSharedPluginData('dsb', 'run_id', RUN_ID);
      v.setSharedPluginData('dsb', 'key', `primitive/${name}`);
      created.push({ name, id: v.id });
    }

return { created, count: created.length };
```

**Critical scope rule for primitives:** Set `v.scopes = []`. This hides primitives from every picker. Designers should only see semantic tokens. The exception is semi-transparent overlay primitives (Black/White with alpha) — those get `["EFFECT_COLOR"]` so they appear in shadow pickers.

### Creating FLOAT Variables (Spacing, Radius, Font Size)

```javascript
const RUN_ID = "ds-build-2024-001";
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const spacingColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/spacing');
if (!spacingColl) throw new Error("Spacing collection not found");
const valueMode = spacingColl.modes[0].modeId;

const spacingTokens = [
  { name: 'spacing/xs',  value: 4,  scope: 'GAP', cssVar: '--spacing-xs' },
  { name: 'spacing/sm',  value: 8,  scope: 'GAP', cssVar: '--spacing-sm' },
  { name: 'spacing/md',  value: 16, scope: 'GAP', cssVar: '--spacing-md' },
  { name: 'spacing/lg',  value: 24, scope: 'GAP', cssVar: '--spacing-lg' },
  { name: 'spacing/xl',  value: 32, scope: 'GAP', cssVar: '--spacing-xl' },
  { name: 'spacing/2xl', value: 48, scope: 'GAP', cssVar: '--spacing-2xl' },
];

const radiusTokens = [
  { name: 'radius/none', value: 0,    scope: 'CORNER_RADIUS', cssVar: '--radius-none' },
  { name: 'radius/sm',   value: 4,    scope: 'CORNER_RADIUS', cssVar: '--radius-sm' },
  { name: 'radius/md',   value: 8,    scope: 'CORNER_RADIUS', cssVar: '--radius-md' },
  { name: 'radius/lg',   value: 16,   scope: 'CORNER_RADIUS', cssVar: '--radius-lg' },
  { name: 'radius/full', value: 9999, scope: 'CORNER_RADIUS', cssVar: '--radius-full' },
];

const created = [];
for (const { name, value, scope, cssVar } of [...spacingTokens, ...radiusTokens]) {
  const v = figma.variables.createVariable(name, spacingColl, 'FLOAT');
  v.setValueForMode(valueMode, value);
  v.scopes = [scope];
  v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
  v.setSharedPluginData('dsb', 'run_id', RUN_ID);
  v.setSharedPluginData('dsb', 'key', name);
  created.push({ name, value, id: v.id });
}

return { created, count: created.length };
```

### Creating STRING Variables (Font Family, Font Style)

```javascript
const RUN_ID = "ds-build-2024-001";
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const typoPrimColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/typography-primitives');
if (!typoPrimColl) throw new Error("Typography Primitives collection not found");
const valueMode = typoPrimColl.modes[0].modeId;

const fontTokens = [
  { name: 'family/sans',  value: 'Inter',       scope: 'FONT_FAMILY', cssVar: '--font-family-sans' },
  { name: 'family/mono',  value: 'Roboto Mono',  scope: 'FONT_FAMILY', cssVar: '--font-family-mono' },
  // Font style strings — these are the Figma fontName.style values:
  { name: 'weight/regular',  value: 'Regular',   scope: 'FONT_STYLE',  cssVar: '--font-weight-regular' },
  { name: 'weight/medium',   value: 'Medium',    scope: 'FONT_STYLE',  cssVar: '--font-weight-medium' },
  { name: 'weight/semibold', value: 'Semi Bold', scope: 'FONT_STYLE',  cssVar: '--font-weight-semibold' },
  { name: 'weight/bold',     value: 'Bold',      scope: 'FONT_STYLE',  cssVar: '--font-weight-bold' },
];

const created = [];
for (const { name, value, scope, cssVar } of fontTokens) {
  const v = figma.variables.createVariable(name, typoPrimColl, 'STRING');
  v.setValueForMode(valueMode, value);
  v.scopes = [scope];
  v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
  v.setSharedPluginData('dsb', 'run_id', RUN_ID);
  v.setSharedPluginData('dsb', 'key', `typo-prim/${name}`);
  created.push({ name, value, id: v.id });
}

return { created, count: created.length };
```

### Creating BOOLEAN Variables

BOOLEAN variables have no scopes (scopes are not supported for BOOLEAN type).

```javascript
const RUN_ID = "ds-build-2024-001";
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const coll = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/tokens');
if (!coll) throw new Error("Collection not found");
const valueMode = coll.modes[0].modeId;

const v = figma.variables.createVariable('feature-flags/show-beta-badge', coll, 'BOOLEAN');
v.setValueForMode(valueMode, false);
// No scopes — BOOLEAN does not support scopes
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
v.setSharedPluginData('dsb', 'key', 'feature-flags/show-beta-badge');

return { id: v.id, name: v.name };
```

---

## 4. Variable Aliasing (VARIABLE_ALIAS) — Primitive → Semantic Chain

Semantic tokens reference primitives via `VARIABLE_ALIAS`. This is the core pattern that makes light/dark theming work.

**Architecture:**
```
Color Primitives collection (1 mode: Value)
  blue/500 = #3B82F6          ← raw value

Color collection (2 modes: Light, Dark)
  color/bg/accent/default:
    Light → VARIABLE_ALIAS → Primitives/blue/500
    Dark  → VARIABLE_ALIAS → Primitives/blue/300
```

### Complete Semantic Alias Creation Script (SDS-style)

```javascript
function hexToRgb(hex) {
  const c = hex.replace('#', '');
  return { r: parseInt(c.slice(0,2),16)/255, g: parseInt(c.slice(2,4),16)/255, b: parseInt(c.slice(4,6),16)/255 };
}

const RUN_ID = "ds-build-2024-001";
const collections = await figma.variables.getLocalVariableCollectionsAsync();

const primColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/primitives');
const colorColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/color');
if (!primColl || !colorColl) throw new Error("Collections not found — run primitive/color collection creation first");

const primValueMode = primColl.modes[0].modeId;
const lightModeId = colorColl.modes.find(m => m.name === 'Light').modeId;
const darkModeId = colorColl.modes.find(m => m.name === 'Dark').modeId;

// Load all primitive variables for lookup
const allVars = await figma.variables.getLocalVariablesAsync();
const primsByKey = {};
for (const v of allVars) {
  if (v.variableCollectionId === primColl.id) {
    primsByKey[v.getSharedPluginData('dsb', 'key')] = v;
  }
}

function getPrim(name) {
  const v = primsByKey[`primitive/${name}`];
  if (!v) throw new Error(`Primitive not found: primitive/${name}`);
  return v;
}

// Define semantic → [lightPrimitiveName, darkPrimitiveName]
// Following the SDS pattern: Background/{Intent}/{Emphasis}
const semanticColors = [
  // Background
  { name: 'color/bg/default/default',   lightPrim: 'white/1000', darkPrim: 'gray/900',
    cssVar: '--color-bg-default-default', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
  { name: 'color/bg/default/secondary', lightPrim: 'gray/100', darkPrim: 'gray/800',
    cssVar: '--color-bg-default-secondary', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
  { name: 'color/bg/brand/default',     lightPrim: 'blue/600', darkPrim: 'blue/300',
    cssVar: '--color-bg-brand-default', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
  // Text
  { name: 'color/text/default/default', lightPrim: 'gray/900', darkPrim: 'white/1000',
    cssVar: '--color-text-default-default', scopes: ['TEXT_FILL'] },
  { name: 'color/text/default/secondary', lightPrim: 'gray/500', darkPrim: 'gray/400',
    cssVar: '--color-text-default-secondary', scopes: ['TEXT_FILL'] },
  { name: 'color/text/brand/default',   lightPrim: 'blue/700', darkPrim: 'blue/200',
    cssVar: '--color-text-brand-default', scopes: ['TEXT_FILL'] },
  // Border
  { name: 'color/border/default/default', lightPrim: 'gray/300', darkPrim: 'gray/600',
    cssVar: '--color-border-default-default', scopes: ['STROKE_COLOR'] },
  { name: 'color/border/brand/default',   lightPrim: 'blue/500', darkPrim: 'blue/400',
    cssVar: '--color-border-brand-default', scopes: ['STROKE_COLOR'] },
];

const created = [];
for (const { name, lightPrim, darkPrim, cssVar, scopes } of semanticColors) {
  const v = figma.variables.createVariable(name, colorColl, 'COLOR');
  // Alias to primitive in Light mode
  v.setValueForMode(lightModeId, figma.variables.createVariableAlias(getPrim(lightPrim)));
  // Alias to primitive in Dark mode
  v.setValueForMode(darkModeId, figma.variables.createVariableAlias(getPrim(darkPrim)));
  // Set scopes (semantic layer — these ARE shown in pickers)
  v.scopes = scopes;
  // Code syntax
  v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
  v.setSharedPluginData('dsb', 'run_id', RUN_ID);
  v.setSharedPluginData('dsb', 'key', name);
  created.push({ name, id: v.id });
}

return { created, count: created.length };
```

**Key API points:**
- `figma.variables.createVariableAlias(variable)` — takes a Variable object, returns `{type:'VARIABLE_ALIAS', id: variable.id}`
- The aliased variable MUST have the same `resolvedType` as the semantic variable
- Never duplicate raw values in the semantic layer — always alias

---

## 5. Variable Scopes — Complete Reference Table

| Semantic Role | Recommended Scopes | Variable Type |
|---|---|---|
| Primitive colors (raw) | `[]` — empty, hidden from all pickers | COLOR |
| Semi-transparent overlay primitives | `["EFFECT_COLOR"]` | COLOR |
| Background fills (frame, shape) | `["FRAME_FILL", "SHAPE_FILL"]` | COLOR |
| Text color | `["TEXT_FILL"]` | COLOR |
| Icon / shape fill | `["SHAPE_FILL", "STROKE_COLOR"]` | COLOR |
| Border / stroke color | `["STROKE_COLOR"]` | COLOR |
| Background + border combined | `["FRAME_FILL", "SHAPE_FILL", "STROKE_COLOR"]` | COLOR |
| Shadow color | `["EFFECT_COLOR"]` | COLOR |
| Spacing / gap between items | `["GAP"]` | FLOAT |
| Padding (if separate from gap) | `["GAP"]` | FLOAT |
| Corner radius | `["CORNER_RADIUS"]` | FLOAT |
| Width / height dimensions | `["WIDTH_HEIGHT"]` | FLOAT |
| Font size | `["FONT_SIZE"]` | FLOAT |
| Line height | `["LINE_HEIGHT"]` | FLOAT |
| Letter spacing | `["LETTER_SPACING"]` | FLOAT |
| Font weight (numeric) | `["FONT_WEIGHT"]` | FLOAT |
| Stroke width | `["STROKE_FLOAT"]` | FLOAT |
| Effect blur radius | `["EFFECT_FLOAT"]` | FLOAT |
| Opacity | `["OPACITY"]` | FLOAT |
| Font family | `["FONT_FAMILY"]` | STRING |
| Font style (e.g. "Semi Bold") | `["FONT_STYLE"]` | STRING |
| Boolean flags | *(scopes not supported)* | BOOLEAN |

**Never use `ALL_SCOPES`** on any variable. It pollutes every picker with irrelevant tokens. The Simple Design System (SDS), the gold standard, uses targeted scopes on every variable.

**`ALL_FILLS` note:** `ALL_FILLS` is exclusive among fill scopes — it covers `FRAME_FILL`, `SHAPE_FILL`, and `TEXT_FILL` together. If set, you cannot also add individual fill scopes. Prefer specifying individual scopes for precision.

### Batch Scope-Setting (After Variables are Created)

If you created variables without scopes and need to set them in batch:

```javascript
const allVars = await figma.variables.getLocalVariablesAsync();

// Scope mapping: partial name match → scopes
const scopeRules = [
  { match: 'color/bg/',     scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
  { match: 'color/text/',   scopes: ['TEXT_FILL'] },
  { match: 'color/icon/',   scopes: ['SHAPE_FILL', 'STROKE_COLOR'] },
  { match: 'color/border/', scopes: ['STROKE_COLOR'] },
  { match: 'spacing/',      scopes: ['GAP'] },
  { match: 'radius/',       scopes: ['CORNER_RADIUS'] },
  { match: 'blue/',         scopes: [] },   // primitives — hide
  { match: 'gray/',         scopes: [] },
  { match: 'white/',        scopes: [] },
  { match: 'black/',        scopes: [] },
];

const updated = [];
for (const v of allVars) {
  if (v.remote) continue; // skip library variables
  for (const rule of scopeRules) {
    if (v.name.startsWith(rule.match)) {
      v.scopes = rule.scopes;
      updated.push({ name: v.name, scopes: rule.scopes });
      break;
    }
  }
}

return { updated, count: updated.length };
```

---

## 6. Code Syntax — WEB/ANDROID/iOS

Every variable must have code syntax set. This is what powers the developer handoff experience:

**What code syntax does:** When a developer inspects any element in Figma Dev Mode that has a variable-bound property (fill, padding, radius, etc.), the code snippet shown uses the variable's code syntax name — not the Figma variable name. For example, a button's background fill bound to `color/bg/primary` will show `background: var(--color-bg-primary)` in the CSS snippet, not `color/bg/primary`. Without code syntax set, Dev Mode shows raw hex values or nothing useful.

You can set up to **3 syntaxes per variable** — one per platform (Web, iOS, Android). Set all three if the codebase targets multiple platforms; set only WEB if it's a web-only project.

```javascript
// WEB: MUST include the var() wrapper — this is the full CSS function syntax
variable.setVariableCodeSyntax('WEB', 'var(--color-bg-primary)');
//                                     ^^^^                   ^
//                              var() wrapper is REQUIRED

// ANDROID: Kotlin property name — camelCase, no wrapper
variable.setVariableCodeSyntax('ANDROID', 'colorBgPrimary');

// iOS: Swift property — dot-notation, no wrapper
variable.setVariableCodeSyntax('iOS', 'Color.bgPrimary');
```

> **CRITICAL — WEB code syntax MUST use the `var()` wrapper.** Setting just `--color-bg-primary` (without `var()`) will cause Dev Mode to show raw hex values instead of the CSS variable reference. Always use the full `var(--name)` form. ANDROID and iOS do NOT use a wrapper.

**Platform derivation rules from the CSS variable name:**

| Platform | Pattern | Example |
|---|---|---|
| WEB | **`var(--{css-var-name})`** — `var()` wrapper required | `var(--sds-color-bg-primary)` |
| ANDROID | camelCase, no wrapper, strip `--` prefix | `sdsColorBgPrimary` |
| iOS | PascalCase after `.`, no wrapper, strip `--` prefix | `Color.SdsColorBgPrimary` or `Color.bgPrimary` |

**Always use the actual CSS variable name from the codebase** — do not derive it from the Figma variable name. If the code uses `--sds-color-background-brand-default`, that exact string is the WEB code syntax (minus the `var()` wrapper that you add).

### Batch Code Syntax Setting

```javascript
const allVars = await figma.variables.getLocalVariablesAsync();
const updated = [];

for (const v of allVars) {
  if (v.remote) continue;
  // If code syntax already set, skip
  if (v.codeSyntax['WEB']) continue;

  // FALLBACK: derive from Figma name: color/bg/primary → var(--color-bg-primary)
  // PREFERRED: pass in a cssVarMap built from actual codebase CSS variable names
  // e.g. cssVarMap = { 'color/bg/primary': '--color-bg-primary', ... }
  const cssName = cssVarMap?.[v.name]
    ?? v.name.replace(/\//g, '-').replace(/\s/g, '-').toLowerCase();
  v.setVariableCodeSyntax('WEB', `var(--${cssName})`);
  updated.push({ name: v.name, web: `var(--${cssName})` });
}

return { updated, count: updated.length };
```

Note: derived names are a fallback only. Always prefer overriding with actual CSS variable names from the codebase when they are known.

---

## 7. Effect Styles (Shadows) and Text Styles

Shadows and composite typography cannot be variables — they are Styles.

### Creating Effect Styles (Shadows)

Reference from SDS (15 effect styles) and the SDS shadow pattern `Shadow/{Level}`:

```javascript
const RUN_ID = "ds-build-2024-001";

// Shadow definitions — CSS equivalent in comments
// CSS: 0 1px 2px rgba(0,0,0,0.05)
const shadows = [
  {
    name: 'Shadow/Subtle',
    effects: [{
      type: 'DROP_SHADOW',
      color: { r: 0, g: 0, b: 0, a: 0.05 },
      offset: { x: 0, y: 1 },
      radius: 2,
      spread: 0,
      visible: true,
      blendMode: 'NORMAL'
    }]
  },
  {
    // CSS: 0 4px 6px -1px rgba(0,0,0,0.10), 0 2px 4px -1px rgba(0,0,0,0.06)
    name: 'Shadow/Medium',
    effects: [
      {
        type: 'DROP_SHADOW',
        color: { r: 0, g: 0, b: 0, a: 0.10 },
        offset: { x: 0, y: 4 },
        radius: 6,
        spread: -1,
        visible: true,
        blendMode: 'NORMAL'
      },
      {
        type: 'DROP_SHADOW',
        color: { r: 0, g: 0, b: 0, a: 0.06 },
        offset: { x: 0, y: 2 },
        radius: 4,
        spread: -1,
        visible: true,
        blendMode: 'NORMAL'
      }
    ]
  },
  {
    // CSS: 0 10px 15px -3px rgba(0,0,0,0.10), 0 4px 6px -2px rgba(0,0,0,0.05)
    name: 'Shadow/Strong',
    effects: [
      {
        type: 'DROP_SHADOW',
        color: { r: 0, g: 0, b: 0, a: 0.10 },
        offset: { x: 0, y: 10 },
        radius: 15,
        spread: -3,
        visible: true,
        blendMode: 'NORMAL'
      },
      {
        type: 'DROP_SHADOW',
        color: { r: 0, g: 0, b: 0, a: 0.05 },
        offset: { x: 0, y: 4 },
        radius: 6,
        spread: -2,
        visible: true,
        blendMode: 'NORMAL'
      }
    ]
  }
];

// M3-style dual shadow (umbra + penumbra pattern):
const m3Shadows = [
  {
    name: 'Elevation/1',
    effects: [
      { type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:2, spread:0, visible:true, blendMode:'NORMAL' },
      { type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:1}, radius:3, spread:1, visible:true, blendMode:'NORMAL' }
    ]
  },
  {
    name: 'Elevation/2',
    effects: [
      { type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:2, spread:0, visible:true, blendMode:'NORMAL' },
      { type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:2}, radius:6, spread:2, visible:true, blendMode:'NORMAL' }
    ]
  },
  {
    name: 'Elevation/3',
    effects: [
      { type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:3, spread:0, visible:true, blendMode:'NORMAL' },
      { type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:4}, radius:8, spread:3, visible:true, blendMode:'NORMAL' }
    ]
  }
];

const created = [];
for (const { name, effects } of shadows) {
  const style = figma.createEffectStyle();
  style.name = name;
  style.effects = effects;
  style.setSharedPluginData('dsb', 'run_id', RUN_ID);
  style.setSharedPluginData('dsb', 'key', `effect-style/${name}`);
  created.push({ name, id: style.id });
}

return { created, count: created.length };
```

### Creating Text Styles

Fonts must be loaded before creating text styles.

```javascript
const RUN_ID = "ds-build-2024-001";

// Define text styles — based on SDS typography hierarchy
const textStyles = [
  // Display / Hero
  { name: 'Display/Hero',    family: 'Inter', style: 'Bold',      size: 72, lineHeight: 80, letterSpacing: -1.5 },
  // Headings
  { name: 'Heading/H1',      family: 'Inter', style: 'Bold',      size: 48, lineHeight: 56, letterSpacing: -1.0 },
  { name: 'Heading/H2',      family: 'Inter', style: 'Bold',      size: 40, lineHeight: 48, letterSpacing: -0.5 },
  { name: 'Heading/H3',      family: 'Inter', style: 'Semi Bold', size: 32, lineHeight: 40, letterSpacing: 0 },
  { name: 'Heading/H4',      family: 'Inter', style: 'Semi Bold', size: 24, lineHeight: 32, letterSpacing: 0 },
  // Body
  { name: 'Body/Large',      family: 'Inter', style: 'Regular',   size: 18, lineHeight: 28, letterSpacing: 0 },
  { name: 'Body/Medium',     family: 'Inter', style: 'Regular',   size: 16, lineHeight: 24, letterSpacing: 0 },
  { name: 'Body/Small',      family: 'Inter', style: 'Regular',   size: 14, lineHeight: 20, letterSpacing: 0 },
  // Label
  { name: 'Label/Large',     family: 'Inter', style: 'Medium',    size: 14, lineHeight: 20, letterSpacing: 0.1 },
  { name: 'Label/Medium',    family: 'Inter', style: 'Medium',    size: 12, lineHeight: 16, letterSpacing: 0.5 },
  { name: 'Label/Small',     family: 'Inter', style: 'Medium',    size: 11, lineHeight: 16, letterSpacing: 0.5 },
  // Code
  { name: 'Code/Base',       family: 'Roboto Mono', style: 'Regular', size: 14, lineHeight: 20, letterSpacing: 0 },
];

// Load all required fonts first
const fontSet = new Set(textStyles.map(s => JSON.stringify({ family: s.family, style: s.style })));
await Promise.all([...fontSet].map(f => figma.loadFontAsync(JSON.parse(f))));

const created = [];
for (const { name, family, style, size, lineHeight, letterSpacing } of textStyles) {
  const ts = figma.createTextStyle();
  ts.name = name;
  ts.fontName = { family, style };
  ts.fontSize = size;
  ts.lineHeight = { value: lineHeight, unit: 'PIXELS' };
  ts.letterSpacing = { value: letterSpacing, unit: 'PIXELS' };
  ts.setSharedPluginData('dsb', 'run_id', RUN_ID);
  ts.setSharedPluginData('dsb', 'key', `text-style/${name}`);
  created.push({ name, id: ts.id });
}

return { created, count: created.length };
```

---

## 8. Idempotency — Check-Before-Create Pattern

Every creation script should check whether the entity already exists before creating it. This prevents duplicates when a script is re-run after partial failure.

### Check-Before-Create for Collections

```javascript
const DSB_KEY = 'collection/primitives';
const RUN_ID = "ds-build-2024-001";

// Check if already exists
const existing = await figma.variables.getLocalVariableCollectionsAsync();
let primColl = existing.find(c => c.getSharedPluginData('dsb', 'key') === DSB_KEY);

if (primColl) {
  return { status: 'already_exists', collectionId: primColl.id, name: primColl.name };
}

// Create only if not found
primColl = figma.variables.createVariableCollection("Primitives");
primColl.renameMode(primColl.modes[0].modeId, "Value");
primColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
primColl.setSharedPluginData('dsb', 'key', DSB_KEY);

return { status: 'created', collectionId: primColl.id };
```

### Check-Before-Create for Variables

```javascript
const VARIABLE_KEY = 'primitive/blue/500';
const RUN_ID = "ds-build-2024-001";

// Check if already exists by sharedPluginData key
const allVars = await figma.variables.getLocalVariablesAsync();
const existing = allVars.find(v => v.getSharedPluginData('dsb', 'key') === VARIABLE_KEY);

if (existing) {
  return { status: 'already_exists', id: existing.id, name: existing.name };
}

// ... create the variable ...
return { status: 'created' };
```

### sharedPluginData Tagging Strategy

Tag every created node immediately after creation. The `key` is the stable logical identifier used for idempotency checks. The `run_id` identifies which build run created it (useful for cleanup).

```javascript
node.setSharedPluginData('dsb', 'run_id', RUN_ID);       // build run ID
node.setSharedPluginData('dsb', 'phase', 'phase1');       // which phase
node.setSharedPluginData('dsb', 'key', 'color/bg/primary'); // stable logical key
```

**Cleanup by run ID (safe — targets only tagged nodes, never user-owned nodes):**

```javascript
const TARGET_RUN_ID = "ds-build-2024-001"; // run to remove
const allVars = await figma.variables.getLocalVariablesAsync();
const removed = [];
for (const v of allVars) {
  if (v.getSharedPluginData('dsb', 'run_id') === TARGET_RUN_ID) {
    removed.push(v.name);
    v.remove();
  }
}
return { removed, count: removed.length };
```

**Never clean up by name prefix** (e.g., deleting everything starting with `color/`). This will destroy user-created variables that happen to share the prefix.

---

## 9. Validation — Verify Counts, Aliases, and Scopes

Run these scripts after Phase 1 to verify everything was created correctly before proceeding to Phase 2.

### Verify Collection and Variable Counts

```javascript
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const allVars = await figma.variables.getLocalVariablesAsync();

const summary = collections.map(c => {
  const vars = allVars.filter(v => v.variableCollectionId === c.id);
  return {
    name: c.name,
    id: c.id,
    modes: c.modes.map(m => m.name),
    variableCount: vars.length,
    missingScopes: vars.filter(v => v.scopes.length === 0 && v.resolvedType !== 'BOOLEAN').length,
    missingCodeSyntax: vars.filter(v => !v.codeSyntax['WEB'] && !v.remote).length,
    sampleVariables: vars.slice(0, 3).map(v => v.name)
  };
});

return {
  collectionCount: collections.length,
  totalVariables: allVars.length,
  collections: summary
};
```

Interpret: `missingScopes > 0` (for non-primitives and non-BOOLEANs) → scope-setting failed, re-run scope script. `missingCodeSyntax > 0` → code syntax not set, run batch code syntax script.

Note: primitives correctly have `scopes = []` (empty, hidden). `missingScopes` above counts non-BOOLEAN variables with empty scopes — review the list to confirm they are all primitives.

### Verify Aliases Resolve

```javascript
const allVars = await figma.variables.getLocalVariablesAsync();
const collections = await figma.variables.getLocalVariableCollectionsAsync();

const brokenAliases = [];
const aliasedVars = [];

for (const v of allVars) {
  if (v.remote) continue;
  const coll = collections.find(c => c.id === v.variableCollectionId);
  if (!coll) continue;

  for (const [modeId, val] of Object.entries(v.valuesByMode)) {
    if (val && typeof val === 'object' && val.type === 'VARIABLE_ALIAS') {
      aliasedVars.push({ name: v.name, aliasTargetId: val.id });
      // Verify the target exists
      const target = allVars.find(t => t.id === val.id);
      if (!target) {
        brokenAliases.push({ variable: v.name, modeId, missingTargetId: val.id });
      }
    }
  }
}

return {
  totalAliased: aliasedVars.length,
  brokenAliases,
  brokenCount: brokenAliases.length,
  status: brokenAliases.length === 0 ? 'all_aliases_resolve' : 'BROKEN_ALIASES_FOUND'
};
```

Interpret: `brokenCount > 0` means a semantic variable references a primitive that was deleted or not yet created. Create the missing primitives, then re-run alias creation for the affected semantic variables.

### Verify Style Counts

```javascript
const [textStyles, effectStyles] = await Promise.all([
  figma.getLocalTextStylesAsync(),
  figma.getLocalEffectStylesAsync()
]);

return {
  textStyles: textStyles.map(s => ({ name: s.name, fontSize: s.fontSize, fontFamily: s.fontName.family })),
  effectStyles: effectStyles.map(s => ({ name: s.name, effectCount: s.effects.length })),
  counts: { text: textStyles.length, effect: effectStyles.length }
};
```

### Phase 1 Exit Criteria Checklist

Before proceeding to Phase 2, verify all of the following:

- Every planned collection exists with the correct number of modes
- Primitive variables: `scopes = []`, code syntax set
- Semantic variables: targeted scopes set, code syntax set, aliases pointing to primitives (not raw values)
- All broken alias count = 0
- All planned text styles exist with correct font family/size/weight
- All planned effect styles exist with correct shadow values
- No variable has `ALL_SCOPES` unless explicitly approved by the user
