@ammduncan/easel 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,9 +28,9 @@ Triggers are **content-shape**, not user-phrasing. If the answer would be a wall
28
28
 
29
29
  ## Tools
30
30
 
31
- - **`push({ html, title?, kind? })`** — appends a card. `html` is required; the wrapper sandboxes it in an iframe and injects a baseline design system. `title` is shown in the card header. `kind` is a freeform tag for the chip (`mockup`, `diff`, `explanation`, `comparison`, `diagram`, `progress`, `status`, etc.).
31
+ - **`push({ html, title?, kind?, theme? })`** — appends a card. `html` is required; the wrapper sandboxes it in an iframe and injects a baseline design system. `title` is shown in the card header. `kind` is a freeform tag for the chip (`mockup`, `diff`, `explanation`, `comparison`, `diagram`, `progress`, `status`, etc.). `theme` (`light`|`dark`) seals the card's canvas mode — frozen at push time, never flips with the global toggle; omit to snapshot the current global theme.
32
32
  - **`open()`** — force-open a fresh browser tab for this session.
33
- - **`config({ preset?, theme?, density? })`** — switch palette, mode, or layout live. Presets: `paper` | `aurora` | `slate`. Themes: `light` | `dark`. Density: `carded` | `flat`.
33
+ - **`config({ preset?, theme?, density? })`** — set Easel's own chrome + the default for FUTURE pushes. Presets: `paper` | `aurora` | `slate`. Themes: `light` | `dark`. Density: `carded` | `flat`. Note: this does NOT restyle existing cards — each is a frozen snapshot. To change a card's mode, re-push with `theme`.
34
34
  - **`label({ label })`** — name the session ("Roadworthy 401 fix"). Call early once the task focus is clear; re-call when the theme shifts. Pass `""` to clear.
35
35
 
36
36
  Reply in chat with **one line**: `pushed to easel ↗ — #N`. Do not restate the content.
@@ -84,77 +84,74 @@ Don't pack content. Space carries the rhythm of a presentation.
84
84
 
85
85
  ### 3. Color
86
86
 
87
- - **The host iframe owns the canvas.** Don't paint `background` on `body` it fights the host and shows up as a wrong-shade block when the user is in the opposite mode. *Exception:* if a particular push really needs to be dark regardless of host (a code-heavy push, say), then OWN the canvas — `background: #0b0f17; color: #e5e7eb;` on `.wrap` — and commit fully to dark.
87
+ - **You own the canvas commit to one mode.** A pushed card is an immutable snapshot, sealed at push time; it never flips when the user toggles Easel's global light/dark theme. So pick ONE mode and paint it: set your own `background` AND `color` on `.wrap` (e.g. light: `background: #faf7f0; color: #1a1a1a`dark: `background: #0b0f17; color: #e5e7eb`). Declare the mode with the push tool's `theme: 'light'|'dark'` param when you've been told which to present in; omit it to snapshot the current global theme. (You *may* leave the surface to the host — the card body behind `.wrap` is frozen too but painting it yourself is the safe default and what "own the canvas" means.)
88
88
  - **One accent only.** Use the wrapper's `var(--ds-accent)` token (it adapts to whichever preset the user picked). Limit accent uses to **3–4 per push**.
89
89
  - **Status colors** (red / amber / green) only when content actually maps to status — not as decoration.
90
90
  - **Page background is never pure white.** The wrapper uses an off-white (`#fafafa`-ish) or a deep dark depending on the host. If you must paint a card surface inside the push, use `var(--ds-surface)`.
91
91
 
92
- ### 4. Light / dark — make EVERYTHING adaptive
92
+ ### 4. Light / dark — commit to ONE mode, no responsiveness
93
93
 
94
- Because the host owns the canvas color, **text color and any local card backgrounds must also respond to the user's light/dark mode**. Hardcoding `color: #111` puts black text on a dark canvas and the whole push goes invisible this has bitten us repeatedly.
94
+ The card is frozen to a single mode, so **don't make it adapt**. Pick light or dark, paint concrete colours for that one mode, and stop. Writing `light-dark()` / `prefers-color-scheme` for the page surface is dead weight — the card won't flip, so the adaptive branch never earns its keep and just risks the wrong half winning. The thing that bit us before — hardcoding `color: #111` onto a *host-controlled* canvas that could be dark can't happen anymore, because you now control the background too: set them as a pair.
95
95
 
96
- Use CSS `light-dark()` to swap:
96
+ Paint the one mode you chose:
97
97
 
98
98
  ```html
99
99
  <style>
100
- :root { color-scheme: light dark; }
101
100
  .wrap {
102
- color: light-dark(#111, #e5e7eb);
101
+ background: #faf7f0; /* light card — pick your surface */
102
+ color: #1a1a1a; /* ink to match it */
103
103
  padding: 56px 40px 96px;
104
104
  font-family: -apple-system, 'Inter', system-ui, sans-serif;
105
105
  }
106
- /* Scope inheritance so the wrapper's text color reaches every child */
107
- .wrap *, .wrap h1, .wrap h2, .wrap p, .wrap li, .wrap span,
108
- .wrap div, .wrap b, .wrap em { color: inherit; }
106
+ /* Push the wrapper's ink into every child so a frame default never leaks.
107
+ ZERO-specificity (:where) so it neutralises UA link/button colours but is
108
+ beaten by ANY authored container colour a hand-rolled dark callout keeps
109
+ its own ink instead of inheriting .wrap's and vanishing on its dark bg. */
110
+ :where(.wrap) :where(*) { color: inherit; }
109
111
 
110
- /* Cards float above whatever canvas the tool gives us — also adapt */
111
112
  .card {
112
- background: light-dark(#fff, #111827);
113
- border: 1px solid light-dark(#e5e5e5, #1f2937);
113
+ background: #ffffff;
114
+ border: 1px solid #e0d9c3;
114
115
  border-radius: 12px;
115
116
  padding: 24px;
116
117
  }
117
-
118
- /* Same treatment for any badge, chip, accent shade you'd previously hardcode */
119
- .badge {
120
- background: light-dark(#f0fdf4, #052e16);
121
- color: light-dark(#028043, #6ee7b7);
122
- }
118
+ .badge { background: #f0fdf4; color: #028043; }
123
119
  </style>
124
120
  <div class="wrap">…</div>
125
121
  ```
126
122
 
127
- **Locked-mode containers must lock their own text color too.** Any container that paints a *fixed*, non-adaptive background a terminal/code block locked to dark, an always-dark callout, a hero filled with a brand color MUST also set its own text color AND re-scope `color: inherit` to its children. Otherwise it inherits `.wrap`'s `light-dark()` and the text flips to the wrong shade for that container's background in one of the two modes.
123
+ (Dark card: `.wrap { background:#0b0f17; color:#e5e7eb }`, `.card { background:#111827; border-color:#1f2937 }`, `.badge { background:#052e16; color:#6ee7b7 }`.) The `--ds-*` tokens still work if you prefer themthey resolve once to the card's frozen mode and stay put but for a self-owned canvas concrete colours are clearer.
124
+
125
+ **Background and ink are always a pair.** Any container that paints its own background — a terminal/code block locked to dark, a callout, a hero filled with a brand color, a white panel on a dark card — MUST also set its own text color (and re-scope `color: inherit` to its children for nested elements). Otherwise it inherits `.wrap`'s committed ink, which may not suit that container's background (dark ink on a dark hero → invisible). (As long as you use the zero-specificity `:where(.wrap) :where(*)` adoption rule above, a single class on the container — `.tip { background:#111; color:#eee }` — is enough; its colour now wins. The old element-qualified form `.wrap div { color: inherit }` was `(0,1,1)` and silently OUTRANKED such a container, flipping its text to the canvas ink — black-on-black — which was the recurring "dark block, text invisible" bug. No `!important` needed anymore.)
128
126
 
129
127
  #### App / UI recreations are *always* locked-mode
130
128
 
131
129
  When you're rendering a recreation of a real piece of UI — a mock of an app screen, a component instance, an embedded preview of what the user will actually see — **that mockup owns its theme completely. Don't make it adapt to the host.**
132
130
 
133
- It's a screenshot-equivalent. If the real app is a dark cobalt dashboard with cyan accents, the mockup should be dark cobalt + cyan regardless of whether the user has easel in light or dark mode. If the real app is a warm cream marketing page, the mockup stays warm cream. The host toggle changes the *surrounding explanation*, not the embedded app preview.
131
+ It's a screenshot-equivalent. If the real app is a dark cobalt dashboard with cyan accents, the mockup should be dark cobalt + cyan regardless of the card's own mode. If the real app is a warm cream marketing page, the mockup stays warm cream. This was always the rule for recreations; now the *whole* card works this way (sealed at push time), so a mock just commits to the app's real colours like everything else.
134
132
 
135
133
  Two reasons:
136
134
  1. **Visual fidelity.** A mockup of the app in dark mode looks wrong when paper-light leaks into it. The user is trying to evaluate the implementation, not a translated version of it.
137
135
  2. **It removes a class of bugs.** App previews have lots of nested elements (buttons, chips, table cells, modal overlays) — getting every layer to adapt correctly via `light-dark()` is fragile. Locking the whole island is simpler and more faithful.
138
136
 
139
- How to do it: paint the mockup's outer container with the app's actual `background` and `color`, and re-scope `color: inherit` to every descendant so the host's adaptive text doesn't leak in:
137
+ How to do it: paint the mockup's outer container with the app's actual `background` and `color`, and re-scope `color: inherit` to every descendant so the card's own ink doesn't leak in:
140
138
 
141
139
  ```html
142
140
  <style>
143
141
  .wrap {
144
- /* host-adaptive surrounding prose */
145
- color: light-dark(#111, #e5e7eb);
142
+ /* the card's committed prose mode (one mode, not adaptive) */
143
+ background: #faf7f0; color: #1a1a1a;
146
144
  }
147
- .wrap *, .wrap h1, .wrap h2, .wrap p, .wrap li, .wrap span,
148
- .wrap div, .wrap b, .wrap em { color: inherit; }
145
+ :where(.wrap) :where(*) { color: inherit; } /* zero-specificity: locked islands below keep their own ink */
149
146
 
150
- /* The app mock — LOCKED to the real app's colors, ignores host mode */
147
+ /* The app mock — LOCKED to the real app's colors, independent of the card mode */
151
148
  .app-mock {
152
149
  background: #0a0e1a; /* the app's actual canvas */
153
150
  color: #e5edff; /* the app's actual ink */
154
151
  border-radius: 12px;
155
152
  padding: 32px;
156
153
  }
157
- .app-mock * { color: inherit; } /* re-scope so .wrap's light-dark() can't leak in */
154
+ .app-mock * { color: inherit; } /* re-scope so .wrap's committed ink can't leak in */
158
155
  .app-mock .btn-primary {
159
156
  background: #3b82f6;
160
157
  color: #fff;
@@ -174,7 +171,7 @@ How to do it: paint the mockup's outer container with the app's actual `backgrou
174
171
  </div>
175
172
  ```
176
173
 
177
- Same principle for light-themed apps: lock to the app's cream/white surface, lock the ink, lock every accent. The host toggle moves the prose around the mock; the mock stays put.
174
+ Same principle for light-themed apps: lock to the app's cream/white surface, lock the ink, lock every accent. Both the prose card and the mock inside it are frozen — neither moves when the user toggles Easel's global theme.
178
175
 
179
176
  #### Use the actual values, not approximations
180
177
 
@@ -388,6 +385,8 @@ Reach for the built-in **`.code`** (alias **`.terminal`**) class instead of hand
388
385
 
389
386
  Token classes: `.kw` (keywords) · `.string` · `.fn` (functions) · `.prop` (identifiers/properties) · `.num` · `.comment` · `.muted` · `.accent`. Plain `<pre>`/`<code>` are already safe too (bg + ink token pair). Only hand-roll a custom dark container when neither fits — and then obey the locked-mode pairing rule above.
390
387
 
388
+ **Reserved names — `code` / `terminal` / `window` are easel primitives.** They paint a locked dark background, so reusing them as your *own* class (`<td class="code">`, `<span class="window">`) inherits that dark fill unintentionally. Two protections: (1) the primitive ink is now locked with `!important`, so a collision renders *readable* dark-on-light instead of invisible dark-on-dark; (2) the viewer logs a console warning when a reserved name lands on an inline/table element. Still — for your own inline code or boxes use a different name (e.g. `mono`, or a plain `<code>` with your styling). The canonical primitive forms are the namespaced **`.easel-code` / `.easel-terminal` / `.easel-window`** (or `[data-easel="code|terminal|window"]`); bare names remain as deprecated aliases.
389
+
391
390
  ### Semantic chips
392
391
 
393
392
  ```html
@@ -0,0 +1,161 @@
1
+ # Easel codegen guide
2
+
3
+ A short starter so an easel push lands at presentation scale on the first try,
4
+ instead of being iterated up from a dense, small-font, emoji-laden draft.
5
+
6
+ **This guide is additive.** The `using-easel` SKILL.md already owns the long
7
+ rulebook — the fidelity bar, the type/whitespace/color scales, light/dark +
8
+ locked-mode pairing, the injected `--ds-*` tokens, and the `.window` / `.code` /
9
+ `.terminal` / `.chip` / `.full-bleed` primitives. Read it for any of that. This
10
+ guide adds the **composition** rules the SKILL doesn't make first-class, plus a
11
+ copy-paste scaffold (`easel-base.css`) and an icon sprite (`easel-icons.svg`).
12
+
13
+ > Lineage: the type/space/color floors trace to Lookbook `fundamentals.md`
14
+ > (`F`-rules) and the generic-AI tells to `anti-patterns.md` (`AP`). The
15
+ > `--ds-*` tokens are easel's `light-dark()` adaptation of Lookbook tokens.
16
+
17
+ ---
18
+
19
+ ## Use the kit
20
+
21
+ 1. Inline the contents of **`easel-base.css`** inside a `<style>` at the top of
22
+ the push.
23
+ 2. Inline **`easel-icons.svg`** once (a hidden `<symbol>` sprite), then reference
24
+ icons with `<svg class="ic"><use href="#i-git-branch"/></svg>`.
25
+ 3. Wrap all content in `<div class="wrap">`.
26
+
27
+ Everything keys off the wrapper's `--ds-*` tokens with literal fallbacks, so the
28
+ same file renders correctly **standalone** (for a local render-and-look) and
29
+ **inside easel** (where it inherits the user's preset accent + light/dark).
30
+
31
+ ---
32
+
33
+ ## Reserved class names — don't reuse these for your own elements
34
+
35
+ The wrapper injects **structural primitives** under these class names, and they
36
+ paint a **locked background + ink**. If you put one on your own element (a
37
+ `<td class="code">`, a `<span class="window">`), it inherits the primitive's
38
+ dark fill you didn't ask for:
39
+
40
+ | Reserved | What it is | If you want it, prefer |
41
+ |---|---|---|
42
+ | `code`, `terminal` | locked-dark code block | `easel-code` / `easel-terminal` / `[data-easel="code"]` |
43
+ | `window` (+ `.dark`, `.desktop`) | macOS window chrome | `easel-window` / `[data-easel="window"]` |
44
+ | `chip` (+ `.bug/.ux/.ok/…`) | semantic glow chip | — (use as documented, or your own name) |
45
+ | `full-bleed` | escapes the prose measure | — (use as documented) |
46
+
47
+ **Workaround / rule:** for *your own* inline code, monospace cells, or boxes,
48
+ use a different name — the kit ships **`.mono`** for inline code, and you can use
49
+ a plain `<code>` element with your own styling. Reach for `code`/`terminal`/
50
+ `window` **only** when you actually want that primitive, and prefer the
51
+ namespaced `.easel-*` form. (Bare names still work as deprecated aliases, and
52
+ the wrapper now locks their ink with `!important` so a collision renders
53
+ *readable* dark-on-light instead of invisible — but it's still an unwanted block.
54
+ The viewer logs a console warning when a reserved name lands on an inline/table
55
+ element.)
56
+
57
+ The kit's own generic class names (`card`, `thread`, `callout`, `lanes`,
58
+ `spine`, `node`) are **not** wrapper primitives, so they don't collide with the
59
+ injected CSS — but they *are* generic, so don't also use them as bare names for
60
+ unrelated author elements in the same push.
61
+
62
+ ---
63
+
64
+ ## The 5 composition rules
65
+
66
+ **1. Presentation scale — body ≥ 18px, nothing below 14px.** [F3 / SKILL §1]
67
+ Lede 46 · title 32 · h2 22 · body 18–21 · eyebrow/mono 14. The scaffold's type
68
+ classes already sit here; don't shrink them to fit more in.
69
+
70
+ **2. Don't fight the prose measure — go vertical.** [the max-width lesson]
71
+ A default (non-`mockup`) push clamps content to a readable column. That clamp
72
+ only hurts when you push *horizontal density* into it (a 4-up row, side-by-side
73
+ text) — then font size is the only variable left and it collapses. A vertical
74
+ flow *wants* a narrow column, so the measure stops being an enemy and the body
75
+ type stays large. If you genuinely need full width (a wide table, a real app
76
+ screen, a matrix), **escape** the measure with `kind:"mockup"` instead of
77
+ shrinking type — see *Picking `kind`*.
78
+
79
+ **3. Column count is a function of content, not a default.** [AP20 / SKILL §6–7]
80
+ - A **sequence** of steps → stack vertically (`.spine` + `.connector`).
81
+ - **Parallel siblings** → a grid (`.lanes`, auto-fit), sized so each tile fits
82
+ its content at ≥16px. Two-up is usually the goldilocks for tiles carrying a
83
+ name + one line.
84
+ - Never a **4-up row of paragraphs** (crushes type) and never an **all-vertical
85
+ stack of siblings** (monotonous, endless scroll). Ask: *is the relationship
86
+ sequential or parallel, and how much does each item carry?*
87
+
88
+ **4. A locked-background container sets its OWN text colour — on every node.**
89
+ [SKILL §4]
90
+ Any container that paints a *fixed* background (`.thread`, a brand hero, a dark
91
+ callout) must commit its own `color` and re-scope `color: inherit` to its
92
+ children — background and text are a pair: commit one, commit the other. The
93
+ scaffold's adoption rule is `:where(.wrap) :where(*)` (zero specificity), so a
94
+ single class on your container — `.callout { background:#111; color:#eee }` —
95
+ now wins on its own; no `!important` needed. (Earlier the scaffold used the
96
+ element-qualified `.wrap div` form at `(0,1,1)`, which outranked a single-class
97
+ container colour and silently flipped its text to the canvas ink → invisible
98
+ dark-on-dark. That's fixed.) Use the built-in `.thread` / the wrapper's
99
+ `.window` / `.code` where they fit — they already lock both.
100
+
101
+ **5. SVG icons, never emoji; colour must mean something — and never rank equal
102
+ siblings.** [AP23 / AP24 / P-AS-01 / F17]
103
+ Pull icons from `easel-icons.svg` (`currentColor`, inherits the chip's colour).
104
+ Two traps, both AI-slop:
105
+ - **Status hues on non-status content.** The `.t-success/-warning/-danger` tones
106
+ are for genuine *status*. Don't paint *categories* (Ignore / Answer / Do /
107
+ Ship) in green/amber/blue — "Do-work = green" means nothing; it's decoration
108
+ in a semantics costume. If a real split exists (e.g. *no-work* vs *work*),
109
+ colour THAT — neutral for one group, the single accent for the other — so
110
+ colour carries information. Otherwise leave them all neutral and let the icon
111
+ differentiate.
112
+ - **Emphasising one of N equal siblings.** A coloured left-border / accent fill
113
+ on one tile in a row of peers (the "accent-bordered hero tile" reflex) reads
114
+ as a stuck selection, not hierarchy. Four exits of one decision are equal —
115
+ don't rank them. Only emphasise when something genuinely *is* primary.
116
+
117
+ ---
118
+
119
+ ## Picking `kind`
120
+
121
+ | Content shape | `kind` | Why |
122
+ |---|---|---|
123
+ | Explanation · flow · status · comparison-in-prose | *(omit)* | Keeps the presentation frame: prose measure, `--ds-*` tokens, Inter, light/dark canvas. Compose vertically (rule 2). |
124
+ | A whole push that is **nothing but** one app/UI screen, edge-to-edge | `"mockup"` / `"app"` | Strips the prose-width cap + body padding + tokens so you own every pixel — then you hand-set font, bg, colours, and supply your own page padding. |
125
+
126
+ Mixed (prose **and** an embedded wide specimen)? Omit `kind` and wrap just the
127
+ specimen in `<div class="full-bleed">` — see SKILL "Full-bleed mockups".
128
+
129
+ ---
130
+
131
+ ## Compose a card — the recipe
132
+
133
+ 1. **Hero** — `.kicker` eyebrow → `.lede` → `.deck` (one tight sentence).
134
+ 2. **Identify the shape**: is the body a *sequence* (→ `.spine`) or a set of
135
+ *parallel options* (→ `.lanes`)? Most explainers are a short sequence that
136
+ ends in a fork of parallel outcomes (spine → lanes).
137
+ 3. **Build tangible, not abstract** [SKILL §5]: an incoming message is a
138
+ `.thread`, a guard rail is a `.callout`, a decision is a `.node` with a
139
+ `git-branch` badge — not labeled rectangles with arrows.
140
+ 4. **Glance test**: would a bullet list say this just as well? If yes, the visual
141
+ is decoration — rebuild it or drop it.
142
+
143
+ ---
144
+
145
+ ## Primitive index (in `easel-base.css`)
146
+
147
+ | Class | What | Notes |
148
+ |---|---|---|
149
+ | `.wrap` | canvas | sets text not background; guards inheritance |
150
+ | `.kicker` `.lede` `.deck` `.title` `.h2` `.body` `.muted` | type scale | presentation sizes |
151
+ | `.mono` | inline code/identifier chip | preset accent on soft surface |
152
+ | `.card` / `.node` | surface / surface + icon badge | locked-surface pairing baked in |
153
+ | `.spine` + `.connector` | vertical sequence | `.connector.short` for tighter gaps |
154
+ | `.lanes` + `.lane` | parallel-sibling grid | auto-fit ≥260px; `.t-*` for state tone |
155
+ | `.icchip` | tinted icon chip | drives off `--c` / `--tint` |
156
+ | `.callout` | tinted note/guard | `.info` `.success` `.danger`; default amber |
157
+ | `.thread` | locked-dark message card | sets its own ink on every node |
158
+
159
+ For app/UI mockups, code, and chips that the SKILL already ships
160
+ (`.window` / `.window.dark`, `.code` / `.terminal`, `.chip`), use those — don't
161
+ re-implement them here.
@@ -0,0 +1,176 @@
1
+ /* ===========================================================================
2
+ easel-base.css — presentation-scale starter for easel pushes.
3
+
4
+ HOW TO USE: inline this whole block inside a <style> at the top of a push,
5
+ then compose with the classes below. Wrap everything in <div class="wrap">.
6
+
7
+ Built ON the easel wrapper's injected --ds-* tokens. A pushed card is an
8
+ IMMUTABLE SNAPSHOT: its mode is sealed at push time and never flips when the
9
+ user toggles Easel's global theme, so these tokens (and their light-dark()
10
+ fallbacks) resolve ONCE to the card's frozen mode and stay there. Every token
11
+ has a literal light-dark() fallback, so this file ALSO renders correctly as a
12
+ standalone .html (e.g. when you render-and-look locally before pushing).
13
+ Inside easel the wrapper's tokens win and the card inherits the user's chosen
14
+ accent for free. You OWN this canvas — commit to one mode; don't write
15
+ responsive (light-dark/prefers-color-scheme) CSS expecting the card to adapt.
16
+
17
+ This is the SCAFFOLD. The full rulebook lives in SKILL.md (fidelity bar,
18
+ type/whitespace/color, locked-mode, .window/.code/.chip primitives,
19
+ full-bleed, kind:"mockup"). Read EASEL-GUIDE.md for the 5 composition rules
20
+ this scaffold encodes.
21
+ =========================================================================== */
22
+
23
+ :root { color-scheme: light dark; }
24
+
25
+ /* ---- canvas: frozen to one mode. The host card surface behind .wrap is itself
26
+ sealed at push time, so inheriting it is safe; set your own `background` here
27
+ when you want full control of the surface (a specific paper tint, a dark
28
+ card). Either way the card never flips. ----------------------------------- */
29
+ .wrap {
30
+ font-family: 'Inter', -apple-system, system-ui, sans-serif;
31
+ color: var(--ds-ink, light-dark(#1a1a1a, #e9e9ea));
32
+ line-height: 1.55;
33
+ padding: 8px 4px 40px;
34
+ }
35
+ /* Push the wrapper's text colour into every child so the frame default never
36
+ leaks. ZERO-specificity (:where) on purpose: it still neutralises UA element
37
+ colours (links/buttons) and adopts .wrap's ink for plain text, but is beaten
38
+ by ANY authored container colour — so a hand-rolled dark callout/tooltip
39
+ (`.tip{background:#111;color:#eee}`) keeps its own ink instead of inheriting
40
+ .wrap's light-dark() and going invisible on its own dark background. The old
41
+ element-qualified form (`.wrap div`, (0,1,1)) outranked single-class
42
+ container colours and was the recurring dark-on-dark bug. */
43
+ :where(.wrap) :where(*) { color: inherit; }
44
+ .wrap svg { display: block; }
45
+
46
+ /* ---- type scale — presentation, NEVER below 14px (SKILL §1) --------------- */
47
+ .kicker, .eyebrow {
48
+ font-size: 14px; font-weight: 600; text-transform: uppercase;
49
+ letter-spacing: .14em; color: var(--ds-accent, light-dark(#15803d, #4ade80));
50
+ }
51
+ .lede { font-size: 46px; font-weight: 500; letter-spacing: -.025em; line-height: 1.08; margin: 18px 0 0; }
52
+ .title { font-size: 32px; font-weight: 500; letter-spacing: -.02em; line-height: 1.15; margin: 0; }
53
+ .deck { font-size: 21px; line-height: 1.5; margin: 18px 0 0; max-width: 36ch;
54
+ color: var(--ds-ink-soft, light-dark(#5f5f63, #a0a0a6)); }
55
+ .h2 { font-size: 22px; font-weight: 600; margin: 0; }
56
+ .body { font-size: 19px; }
57
+ .muted { color: var(--ds-muted, light-dark(#6b6b6b, #9a9a9a)); }
58
+
59
+ /* ---- inline mono chip — codes, identifiers, statuses, tokens -------------- */
60
+ .mono {
61
+ font-family: ui-monospace, 'Roboto Mono Variable', monospace; font-size: 14px;
62
+ padding: 2px 8px; border-radius: 6px;
63
+ background: var(--ds-surface-soft, light-dark(#f3f4f6, #1f2329));
64
+ color: var(--ds-accent, light-dark(#15803d, #6ee7a8)) !important;
65
+ border: 1px solid var(--ds-line-soft, light-dark(#e3e5e8, #2c3038));
66
+ }
67
+
68
+ /* ---- card / node (card + leading icon badge + heading) -------------------- */
69
+ .card {
70
+ background: var(--ds-surface, light-dark(#ffffff, #15171c));
71
+ border: 1px solid var(--ds-line, light-dark(#e7e7ea, #2a2d34));
72
+ border-radius: 14px; padding: 24px 26px;
73
+ box-shadow: var(--ds-shadow-md, 0 1px 2px light-dark(rgba(0,0,0,.05), rgba(0,0,0,.3)));
74
+ }
75
+ .card * { color: inherit; } /* locked-surface pairing: surface set → keep text from leaking */
76
+
77
+ .node { display: flex; gap: 18px; align-items: flex-start; }
78
+ .node .badge {
79
+ flex: none; width: 46px; height: 46px; border-radius: 12px;
80
+ display: grid; place-items: center;
81
+ background: var(--ds-accent-soft, light-dark(#f0f7f2, #16241c));
82
+ color: var(--ds-accent, light-dark(#15803d, #4ade80));
83
+ }
84
+ .node .badge svg { width: 24px; height: 24px; }
85
+ .node .h2 + p, .node .h2 ~ p { font-size: 18px; margin: 6px 0 0;
86
+ color: var(--ds-ink-soft, light-dark(#56565a, #9b9ba1)); }
87
+
88
+ /* ===========================================================================
89
+ PRIMITIVE 1 — spine + lanes (sequence vs. parallel siblings)
90
+ RULE: a sequence of steps stacks VERTICALLY (.spine + .connector).
91
+ parallel siblings go in a GRID (.lanes) sized so each fits — never a
92
+ 4-up row of paragraphs, never an all-vertical stack of siblings.
93
+ =========================================================================== */
94
+ .spine { display: flex; flex-direction: column; gap: 14px; }
95
+ .connector {
96
+ width: 2px; height: 26px; margin-left: 31px;
97
+ background: var(--ds-line, light-dark(#d9d9dd, #34373d));
98
+ }
99
+ .connector.short { height: 16px; }
100
+
101
+ /* auto-fit: tiles flow to as many columns as fit at >=260px, then reflow down */
102
+ .lanes { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
103
+ .lane {
104
+ background: var(--ds-surface, light-dark(#ffffff, #15171c));
105
+ border: 1px solid var(--ds-line, light-dark(#e7e7ea, #2a2d34));
106
+ border-radius: 12px; padding: 18px 20px;
107
+ }
108
+ .lane * { color: inherit; }
109
+ .lane .top { display: flex; align-items: center; gap: 12px; }
110
+ .lane .nm { font-size: 19px; font-weight: 600; }
111
+ .lane .ds { font-size: 16px; line-height: 1.5; margin: 12px 0 0;
112
+ color: var(--ds-ink-soft, light-dark(#56565a, #9b9ba1)); }
113
+
114
+ /* icon chip — drives off --c (icon colour) + --tint (chip bg) when a tone is set */
115
+ .icchip {
116
+ flex: none; width: 38px; height: 38px; border-radius: 10px;
117
+ display: grid; place-items: center;
118
+ background: var(--tint, var(--ds-surface-soft, light-dark(#f1f5f9, #1c2129)));
119
+ color: var(--c, var(--ds-accent, light-dark(#15803d, #4ade80)));
120
+ }
121
+ .icchip svg { width: 20px; height: 20px; }
122
+
123
+ /* semantic tones — apply --c / --tint ONLY when a tile genuinely maps to a
124
+ status/state (RULE: status colour where state maps to status, else one accent) */
125
+ .t-neutral { --c: light-dark(#64748b, #94a3b8); --tint: light-dark(#f1f5f9, #1c2129); }
126
+ .t-info { --c: light-dark(#2563eb, #60a5fa); --tint: light-dark(#eef3ff, #12203a); }
127
+ .t-success { --c: light-dark(#15803d, #4ade80); --tint: light-dark(#ecf9f0, #102a1c); }
128
+ .t-warning { --c: light-dark(#b45309, #f59e0b); --tint: light-dark(#fdf4e8, #291d0a); }
129
+ .t-danger { --c: light-dark(#b91c1c, #f87171); --tint: light-dark(#fdecec, #2a1414); }
130
+ /* NO accent-border / emphasis on one sibling: four parallel exits are equal
131
+ weight — singling one out with a coloured bar is AP23 slop (status double-
132
+ encoded, the "accent-bordered tile" AI reflex). Differentiate by icon, not
133
+ by ranking siblings that aren't ranked. */
134
+
135
+ /* ===========================================================================
136
+ PRIMITIVE 2 — callout (tinted, leading icon): guard rails, notes, warnings
137
+ Default = warning amber. Add .info / .success / .danger to retone.
138
+ =========================================================================== */
139
+ .callout {
140
+ display: flex; gap: 16px; align-items: flex-start;
141
+ border-radius: 14px; padding: 20px 24px;
142
+ background: var(--co-bg, light-dark(#fdf8ed, #1d180c));
143
+ border: 1px solid var(--co-bd, light-dark(#f0d9a8, #4a3c1c));
144
+ }
145
+ .callout * { color: inherit; }
146
+ .callout .cico {
147
+ flex: none; width: 40px; height: 40px; border-radius: 10px;
148
+ display: grid; place-items: center;
149
+ background: var(--co-ic-bg, light-dark(#f7ecd0, #2a2110));
150
+ color: var(--co-ic, light-dark(#b45309, #e0a64a));
151
+ }
152
+ .callout .cico svg { width: 22px; height: 22px; }
153
+ .callout .q { font-size: 19px; font-weight: 600;
154
+ color: var(--ds-ink, light-dark(#1a1a1a, #e9e9ea)); }
155
+ .callout .a { font-size: 17px; margin-top: 6px;
156
+ color: var(--co-ic, light-dark(#7a6a3a, #cbb87e)); }
157
+ .callout.info { --co-bg: light-dark(#eef3ff,#0e1a30); --co-bd: light-dark(#cdddfb,#1d3257); --co-ic-bg: light-dark(#dde9ff,#16284a); --co-ic: light-dark(#2563eb,#7fb0ff); }
158
+ .callout.success { --co-bg: light-dark(#ecf9f0,#0c2417); --co-bd: light-dark(#c6e9d3,#1c402c); --co-ic-bg: light-dark(#d6f0e0,#143020); --co-ic: light-dark(#15803d,#74d99a); }
159
+ .callout.danger { --co-bg: light-dark(#fdecec,#240f0f); --co-bd: light-dark(#f5cccc,#4a1f1f); --co-ic-bg: light-dark(#fadada,#341515); --co-ic: light-dark(#b91c1c,#f59a9a); }
160
+
161
+ /* ===========================================================================
162
+ PRIMITIVE 3 — thread / message card (LOCKED DARK)
163
+ It sets its OWN text colour on every node (the .wrap inherit-guard outranks
164
+ a descendant universal, so locked-bg containers must commit their own ink).
165
+ For app/UI mockups use the wrapper's .window / .window.dark instead.
166
+ =========================================================================== */
167
+ .thread {
168
+ background: #14161b; border: 1px solid #262a31; border-radius: 14px;
169
+ overflow: hidden; box-shadow: 0 6px 24px rgba(0,0,0,.22);
170
+ }
171
+ .thread .bar { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-bottom: 1px solid #262a31; }
172
+ .thread .dot { width: 11px; height: 11px; border-radius: 50%; }
173
+ .thread .tt { margin-left: 8px; font-size: 13px; font-family: ui-monospace, monospace; color: #7d828c !important; }
174
+ .thread .body { padding: 16px 20px 20px; }
175
+ .thread .who { font-size: 14px; font-weight: 600; color: #8ab4ff !important; }
176
+ .thread .msg { font-size: 18px; line-height: 1.5; margin: 6px 0 0; color: #e6e9ef !important; }
@@ -0,0 +1,28 @@
1
+ <!-- easel-icons.svg — Lucide (ISC license) · one set · 1.75 stroke · currentColor.
2
+ Inline this whole block once near the top of a push, then reference an icon with:
3
+ <svg class="ic"><use href="#i-git-branch"/></svg>
4
+ and size it with CSS (.icchip svg / .badge svg already do). currentColor means
5
+ the icon inherits its container's `color` — set --c on a .icchip or color on a .badge.
6
+ NEVER use emoji as icons (SKILL fidelity bar / Lookbook P-AS-01). Swap the SET
7
+ wholesale if you change libraries; never mix per-icon. -->
8
+ <svg width="0" height="0" style="position:absolute" aria-hidden="true">
9
+ <symbol id="i-git-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol>
10
+ <symbol id="i-message-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></symbol>
11
+ <symbol id="i-send" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></symbol>
12
+ <symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></symbol>
13
+ <symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09"/><path d="M9 12a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.4 22.4 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 .05 5 .05"/></symbol>
14
+ <symbol id="i-bell-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13.73 21a2 2 0 0 1-3.46 0"/><path d="M18.63 13A17.9 17.9 0 0 1 18 8"/><path d="M6.26 6.26A5.86 5.86 0 0 0 6 8c0 7-3 9-3 9h14"/><path d="M18 8a6 6 0 0 0-9.33-5"/><line x1="2" y1="2" x2="22" y2="22"/></symbol>
15
+ <symbol id="i-ban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></symbol>
16
+ <symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></symbol>
17
+ <symbol id="i-check-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21.801 10A10 10 0 1 1 17 3.335"/><path d="m9 11 3 3L22 4"/></symbol>
18
+ <symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></symbol>
19
+ <symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></symbol>
20
+ <symbol id="i-zap" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></symbol>
21
+ <symbol id="i-arrow-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></symbol>
22
+ <symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></symbol>
23
+ <symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></symbol>
24
+ <symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
25
+ <symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/></symbol>
26
+ <symbol id="i-trending-up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M16 7h6v6"/><path d="m22 7-8.5 8.5-5-5L2 17"/></symbol>
27
+ <symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></symbol>
28
+ </svg>