{
  "$meta": {
    "name": "PLUR design tokens",
    "version": "2.2.0",
    "updated": "2026-08-18",
    "source": "plur.ai hero mark and site stylesheet",
    "note": "Single source of truth. tokens.css and assets/*.svg are GENERATED from this file by build_assets.py \u2014 edit here, never there."
  },
  "color": {
    "dark": {
      "bg": {
        "value": "#0e0f14",
        "role": "Primary ground. Near-black with a blue bias; the accents were chosen against it and drift on true black."
      },
      "surface": {
        "value": "#15171d",
        "role": "Raised panels, cards, table fills."
      },
      "hairline": {
        "value": "rgba(255,255,255,0.08)",
        "role": "Dividers and 1px borders."
      },
      "text": {
        "value": "#f0f0f2",
        "role": "Primary text. 16.8:1 on bg (AAA)."
      },
      "mid": {
        "value": "rgba(255,255,255,0.62)",
        "role": "Body and secondary text. 7.7:1 on bg (AAA)."
      },
      "dim": {
        "value": "rgba(255,255,255,0.55)",
        "role": "Micro-labels, table headers, secondary UI text. TEXT ONLY \u2014 the mark's latent dots use dot-rest.",
        "contrast_on_bg": "6.24:1",
        "wcag": "passes AA at all sizes"
      },
      "dot-rest": {
        "value": "rgba(255,255,255,0.38)",
        "role": "Resting/latent dots in the mark. NOT text \u2014 no contrast floor applies. Split from dim on 2026-08-18 so fixing text contrast does not alter the logo."
      }
    },
    "light": {
      "bg": {
        "value": "#fafaf9",
        "role": "Light ground (site only; brand and video material stay dark)."
      },
      "surface": {
        "value": "#ffffff",
        "role": "Raised panels on light."
      },
      "hairline": {
        "value": "rgba(0,0,0,0.10)",
        "role": "Dividers on light."
      },
      "text": {
        "value": "#1a1a1a",
        "role": "Primary text on light."
      },
      "mid": {
        "value": "rgba(0,0,0,0.64)",
        "role": "Body text on light."
      },
      "dim": {
        "value": "rgba(0,0,0,0.55)",
        "role": "Micro-labels, table headers, folios, captions on light. Raised from 0.45 (3.31:1, failed AA) on 2026-08-19 \u2014 it carries the SMALLEST type in the system, so it needed the highest alpha, not the lowest.",
        "contrast_on_bg": "4.74:1"
      }
    },
    "accent": {
      "cyan": {
        "value": "#22d3ee",
        "node": 1,
        "contrast_on_dark_bg": "10.59:1"
      },
      "amber": {
        "value": "#f0a050",
        "node": 2,
        "contrast_on_dark_bg": "8.98:1"
      },
      "violet": {
        "value": "#a78bfa",
        "node": 3,
        "contrast_on_dark_bg": "7.03:1"
      },
      "emerald": {
        "value": "#34d399",
        "node": 4,
        "contrast_on_dark_bg": "9.96:1"
      },
      "$rule": "Accents are POSITIONAL, not semantic. Cyan is always the first node fired, emerald always the last, whatever grid positions the path uses. Never map them to status or category."
    },
    "accent_on_light": {
      "cyan": {
        "value": "#0b7e90",
        "contrast_on_light_bg": "4.57:1"
      },
      "amber": {
        "value": "#ad5e0f",
        "contrast_on_light_bg": "4.59:1"
      },
      "violet": {
        "value": "#7a50f7",
        "contrast_on_light_bg": "4.66:1"
      },
      "emerald": {
        "value": "#1c805c",
        "contrast_on_light_bg": "4.69:1"
      },
      "$rule": "TEXT ONLY. The display accents fail AA badly on #fafaf9 \u2014 cyan is 1.73:1, amber 2.04, emerald 1.84, violet 2.61 \u2014 so they cannot carry type, links or section coding on light. They remain correct for the MARK, swatches and decorative fills, where no contrast floor applies. Same hue and saturation, lightness reduced until each clears 4.5:1."
    },
    "semantic": {
      "warning": {
        "value": "#e8695f",
        "role": "Prohibitions, destructive actions, error states.",
        "contrast_on_light_bg": "3.42:1 \u2014 large text and rules only, never body copy"
      },
      "$rule": "Semantic colour lives OUTSIDE the four accents by design: the accents encode sequence, so they cannot also encode severity without destroying that meaning. This is the only semantic colour in the system. It is not an accent and never appears in the mark."
    }
  },
  "type": {
    "display": {
      "family": "Outfit",
      "weights": [
        100,
        200,
        300,
        400,
        500,
        600,
        700
      ]
    },
    "mono": {
      "family": "JetBrains Mono",
      "weights": [
        300,
        400
      ]
    },
    "$rule": "Weight 900 is RESERVED for the four letters inside the mark. Nothing else in the system uses it; everywhere else runs 100-400.",
    "$dropped": "Literata was specified as the editorial face but never embedded and never used. Dropped 2026-08-19 rather than carried as an open question \u2014 a face the system cannot render is not part of the system."
  },
  "mark": {
    "viewBox": 200,
    "grid": [
      40,
      100,
      160
    ],
    "home_path": [
      0,
      4,
      5,
      8
    ],
    "node_radius": {
      "resting": 7,
      "fired": 21
    },
    "node_opacity": {
      "resting_min": 0.04,
      "resting_max": 0.7,
      "fired": 0.88
    },
    "bar": {
      "stroke_width": 11,
      "linecap": "round",
      "opacity": 0.72
    },
    "letter": {
      "family": "Outfit",
      "weight": 900,
      "size": 32,
      "cap_height_target": "em 32 at wght 900 \u2014 heavier letterform, smaller relative to the disc so the ring survives",
      "optical_offsets": {
        "P": 1.0,
        "L": -1.3,
        "U": 0.0,
        "R": 0.2,
        "$note": "Per-glyph nudge in viewBox units at size 25, scaled linearly. Bounding-box centring leaves P visually high (mass in the bowl) and L low (mass in the foot bar)."
      },
      "$weight_history": "700 originally -> 600 on the 2026-08-18 tufte audit (R counter/stem 1.13, argued to close under ink spread) -> back to 700 by eye. The audit's concern was real but its prescription overcorrected: it reasoned about the letters at em 25. At em 36 there is more absolute room, and 600 read visibly light. Counters soften below ~48px; the mark's floor is 40px."
    },
    "clear_space": {
      "value": "one grid unit (60 units at viewBox 200, = 30% of mark width)",
      "note": "Measured from the mark's bounding box, not the viewBox edge."
    },
    "min_size": {
      "digital": "48px",
      "print": "10mm",
      "note": "Raised from 40px when the letters went to wght 900 / em 32. Heavier strokes close the R and P counters earlier, and the smaller em compounds it. Verified by render at 28/32/36/40/44/48/56px: mush below 36, marginal at 40, clean at 48."
    },
    "reduced_form": {
      "use_below": "48px digital / 55mm embroidery",
      "letters": "dropped",
      "rationale": "Decided 2026-08-18. Below the floor the mark works by visual recognition \u2014 grid, path shape and colour sequence \u2014 not by spelling. A favicon does not need to be read, it needs to be recognised. This closes the open question of whether the reduced form keeps its letters: it does not."
    },
    "lockup": {
      "mark_form": "reduced \u2014 dots only, no letters",
      "wordmark": "PLUR",
      "wordmark_weight": 300,
      "gap": "one grid unit (60 units at viewBox 200)",
      "rationale": "Decided 2026-08-18, resolving the redundancy the critic evaluator raised. The full mark spells PLUR, so pairing it with a wordmark said the name twice \u2014 and printed the answer next to the puzzle, contradicting the brand's own rule against back-to-back resolution. Stripping the letters makes the mark a pure symbol and lets the type carry the name, so nothing is doubled. It also reuses the reduced four-node form already required below the 48px floor, rather than inventing a third asset.",
      "wordmark_weight_note": "300 rather than the 200 a display role would suggest: a wordmark must survive small reproduction, where 200 goes fragile. The mark supplies the visual weight."
    }
  },
  "motion": {
    "node_step_ms": 500,
    "letter_offset_ms": 160,
    "bar_offset_ms": 260,
    "hold_home_ms": 6000,
    "hold_wander_ms": 3500,
    "dissolve_ms": 900,
    "rest_ms": [
      2500,
      4000
    ],
    "easing": {
      "node": "cubic-bezier(0.4, 0, 0.2, 1)",
      "bar": "ease-out",
      "opacity": "ease-in-out"
    },
    "afterimage": {
      "enabled": true,
      "mode": "negative",
      "$modes": {
        "positive": "Same hue as the stimulus, dimmer and blurred. Perceptually the short-lived afterimage (~0.5s). Brand-safe: introduces no new colour.",
        "negative": "Complementary hue. Perceptually the long afterimage (1-5min), and the truthful one \u2014 but it puts off-palette colour on screen."
      },
      "duration_ms": 1400,
      "peak_opacity": 0.25,
      "blur": 8,
      "expand": 1.18,
      "stagger_ms": 50,
      "complements": {
        "cyan": "#d75a47",
        "amber": "#71a8df",
        "violet": "#d0e986",
        "emerald": "#c3527b"
      },
      "$rationale": "An afterimage IS a memory trace: photoreceptor fatigue leaves an image persisting after the stimulus is gone, which is exactly Semon's 1904 engram \u2014 'the physical trace an experience leaves'. The mark demonstrates the brand's core concept in the viewer's own visual system rather than illustrating it. Duration is tuned to the ~0.5s positive afterimage, so it reads as perception rather than as a fade effect.",
      "$tuned": "Converged by eye in mark-tuner.html on 2026-08-18. Previous first-pass values were {\"mode\": \"positive\", \"duration_ms\": 620, \"peak_opacity\": 0.5, \"blur\": 4.5, \"expand\": 1.28, \"stagger_ms\": 90} \u2014 reference-only, never shipped."
    }
  },
  "video": {
    "coverage_rule": "n_terms * clip_duration >= audio_duration",
    "slot_seconds": 4,
    "tail_seconds": [
      1.5,
      2
    ],
    "music_gain": 0.1,
    "rules": [
      "Never crossfade narration. Speech runs full level to its last word; picture transitions during silence.",
      "Music runs continuously through the end card and fades only in the final 2s.",
      "Pin one music track per campaign. Randomised music destroys audio identity.",
      "Close on a node or network shot so the mark condenses out of the last frame.",
      "Tag stock material by what is in frame, not by narrative beat. Concrete nouns resolve; actions do not.",
      "No identifiable stock people."
    ]
  },
  "$issues": {
    "dim-contrast": {
      "status": "FIXED",
      "found": "2026-08-18 impeccable audit",
      "detail": "color.dark.dim composites to 3.55:1 on bg. Every usage is text under 24px (table headers 10.5px, captions 11px, meta rows 11.5px), which requires 4.5:1.",
      "proposed": "rgba(255,255,255,0.55) -> 5.30:1, clears AA while staying recessive",
      "blast_radius": "plur.ai still ships --text-dim at 0.38 site-wide and now diverges from the brand system until the site follows.",
      "resolved": "2026-08-18",
      "resolution": "dim raised 0.38 -> 0.55 (3.55:1 -> 6.24:1, clears AA). Token split: the mark's latent dots moved to dot-rest at the original 0.38, so the logo is byte-identical. Revert with: git revert <this commit>."
    },
    "letter-legibility": {
      "status": "FIXED",
      "found": "2026-08-18 tufte + critic evaluators, both independently",
      "detail": "Letters were 42% of disc diameter (convention 55-65%) and weight 700 closed the R counter to 1.13x its own stem. R counter rendered 0.71px at the stated 32px minimum. Mark read as four abstract dots below ~64px.",
      "resolution": "letter.size 25->36 and weight 700->600 on the audit, then weight ->900 at em 32 by eye (2026-08-18). The audit's counter concern was real but it reasoned about letters at em 25; with more absolute room, 600 read visibly light. Cost is a higher floor: digital minimum 48px."
    }
  }
}
