# PRISM — rules for agents

PRISM is a light-mode-first design system built on light. One anchor hue
derives the palette. Density chooses a material, and a material says how an
object treats light. Light events say what actions do to light. The light
rests, and moves only for a story beat or an action.

- Reference: https://shupp.dev/design/prismatic
- Variants lab: https://shupp.dev/design/prismatic/lab
- Tokens: /prism/prism.tokens.json · Material CSS: /prism/prism.css
- Light engine (optional, WebGL2): /prism/light.js, with tables in /prism/rules.js

## Setup

```html
<html data-mode="day" style="--h:185; --hr:129">
<link rel="stylesheet" href="/prism/prism.css">
<script type="module" src="/prism/light.js"></script>
```

Set `--hr` = R(`--h`): h if h ≤ 45; round(h − 0.4(h − 45)) if h ≤ 300; else
round((h + 0.4(385 − h)) mod 360).

## Rules

1. **One hue.** One anchor hue derives the four brand bands at fixed offsets. Success 152°, danger 27° and warning 75° never rotate; info follows the anchor.
2. **Night is a redshift.** Night maps the hue with R(h) and swaps figure and ground. It is occlusion first: objects grow more opaque and light is scarce.
3. **Density chooses the material.** Transient is glass, standing is frosted, immovable is pigment. Declare data-material only to say something more.
4. **The light rests.** The key light sits where the rig puts it. It never tracks the pointer.
5. **Motion is evidence.** Light moves for a story beat (sunrise, scroll light) or an action (a light event), then sleeps. Only aria-busy may loop.
6. **Key light shadows; play light colors.** Shadows lean toward the far band. Dense absorbs; light on light tinges; a prism splits.
7. **Legibility is a hard bound.** Day shadows darken the field at most 16%; night glow lifts it at most 0.22. Text on the field may take a halo or a mask, never less than AA.
8. **Data never casts.** Rows hold hairlines. Selection is a 2px seam, lit as a pipe.
9. **State is never color alone.** A word or symbol travels with every status.
10. **The playground holds the pointer.** Only the playground lets the pointer move light.
11. **Display type is large.** Tilt Prism only at 40px or more; Instrument Sans below.
12. **One context per page.** Every stage shares the page’s single WebGL2 context.

## Materials

Declare with `data-material`. With none declared, density decides:
transient → glass, standing → frosted, immovable → pigment.
Tint with `data-tint` (anchor, near, far, counter, danger, success).

| material | T | does | use for |
| --- | --- | --- | --- |
| `glass` | 0.6 | transmits | transient surfaces |
| `prism` | 0.55 | splits incident light into the four bands | primary actions, hero marks |
| `dichroic` | tint | passes its band; glows its complement on the lit side | brand accents |
| `frosted` | 0.3 | transmits without direction and re-emits softly | standing surfaces, disabled controls |
| `filter` | tint | passes only its band, so light on light tinges | selected or tinted states |
| `pigment` | 0.05 | absorbs, bouncing a little of its own color | immovable surfaces |
| `mirror` | 0 | redirects light into a reflected beam | dividers that route light |
| `emitter` | 1 | is a source | screens, live status, hovered controls |
| `phosphor` | 0.5 | stores light and re-emits it over 35q | notifications, recent changes |
| `fog` | 1 | scatters: beams show, and dim with distance | atmosphere, emphasis zones |
| `void` | 0 | absorbs everything | destructive actions, night occlusion |
| `pipe` | 0 | carries light from its lit end along its length | focus, selection seams, progress |
| `film` | 0.8 | shifts color with the angle of the light | foil, celebratory marks |
| `lens` | 0.9 | converges light into a beam | toggles, the current choice |

## Light events

Declare with `data-light-event="emit absorb pulse void reject lift carry afterglow"`,
or call `light.event(el, type)` from /prism/light.js. 1q = 40ms.

| event | on | does | q | kind |
| --- | --- | --- | --- | --- |
| `emit` | hover · focus | the object turns faintly emissive | 3 | held |
| `absorb` | press | the object turns to pigment, then recovers | 3 | timed |
| `pulse` | success | a ring of light disperses outward | 12 | timed |
| `pipe` | focus-visible | a light pipe walks the edge once | 10 | timed |
| `prism` | primary (passive) | the key light splits into bands across it | 0 | passive |
| `void` | destructive press | the object drinks the light near it | 8 | timed |
| `frost` | disabled | the object frosts: light passes, scattered | 0 | passive |
| `lens` | toggle | a lens focuses a beam on the chosen option | 0 | held |
| `seam` | select row | the row seam becomes a lit pipe | 0 | held |
| `sunrise` | first paint | the key light rises to rest, once | 25 | timed |
| `busy` | aria-busy | light breathes only while busy | 22 | held, loops while held |
| `lift` | drag · hold | the object rises: its shadow detaches and softens | 0 | held |
| `reject` | error | the object becomes a danger filter, then clears | 8 | timed |
| `afterglow` | arrival | phosphor afterglow that decays | 35 | timed |
| `carry` | copy · send | light travels to its destination | 10 | timed |
| `eclipse` | mode change | an occluder crosses the key light | 25 | timed |

## Light rig

The rig is a parameter component: `light.setRig({ … })`.

| param | values | default | does |
| --- | --- | --- | --- |
| `source` | window | overhead | footlight | spots | window | where the key light rests |
| `scroll` | on | off | false | the key light travels a day arc as you read |
| `fog` | 0–1 | 0 | scatter: beams show and dim with distance |
| `surfaces` | field | lit | edges | field | where light may land |
| `legibility` | clamp | halo | mask | clamp | how text on the field keeps its contrast |
| `depth` | flat | z | flat | whether z-layer offsets a shadow from its object |
| `night` | occlusion | light | occlusion | night as scarce light, or night as light play |

## Anti-patterns

- **Don’t track the cursor with the light.** Ambient motion while reading. The light rests (§4).
- **Don’t loop an animation at rest.** Only aria-busy loops (§5).
- **Don’t tilt or parallax objects with the pointer.** Nothing was selected, so nothing should move.
- **Don’t show a pointer cursor on an inert row.** The cursor promises an action that is not there.
- **Don’t make inert things focusable.** Focus promises an action too.
- **Don’t pick a material for its looks.** Density picks the material (§3).
- **Don’t cast gray shadows.** Shadows lean toward the far band (§6).
- **Don’t let light cross body text unchecked.** Clamp, halo or mask it (§7).
- **Don’t glow every row.** Data never casts (§8).
- **Don’t mount a WebGL context per component.** One per page; stages share it (§12).

## Acceptance

- [ ] Moving the pointer across the page without clicking changes nothing outside the playground.
- [ ] At rest the frame counter stops: the loop sleeps.
- [ ] Every light event settles within its listed q; held states end with their state.
- [ ] Under prefers-reduced-motion nothing moves, and held states still show.
- [ ] The contrast audit passes AA under full shadow and full glow.
- [ ] Turning the hue re-tints every band and every shadow; semantic hues hold.
- [ ] A filter’s shadow is its color, a pigment’s is dark, and a prism throws four bands.
- [ ] A rig change re-lights the page in one solve, and nothing else moves.
