@motion-proto/live-tokens 0.62.0 → 0.64.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.
Files changed (71) hide show
  1. package/.claude/skills/live-tokens-adjust-geometry/SKILL.md +4 -4
  2. package/.claude/skills/live-tokens-build-page/SKILL.md +21 -3
  3. package/.claude/skills/live-tokens-create-component/SKILL.md +16 -48
  4. package/.claude/skills/live-tokens-create-component/references/fixed-overlays.md +10 -1
  5. package/.claude/skills/live-tokens-create-component/references/intrinsics.md +7 -5
  6. package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +1 -1
  7. package/.claude/skills/live-tokens-create-component/references/token-naming.md +50 -0
  8. package/.claude/skills/live-tokens-generate-theme/SKILL.md +16 -9
  9. package/.claude/skills/live-tokens-pair-fonts/SKILL.md +8 -6
  10. package/.claude/skills/live-tokens-pick-component/SKILL.md +19 -5
  11. package/CHANGELOG.md +100 -0
  12. package/bin/migrate.mjs +6 -2
  13. package/dist-plugin/adjust/index.cjs +1 -1
  14. package/dist-plugin/adjust/index.d.cts +1 -1
  15. package/dist-plugin/adjust/index.d.ts +1 -1
  16. package/dist-plugin/adjust/index.js +1 -1
  17. package/dist-plugin/{chunk-232GZGQU.js → chunk-NDJJORKJ.js} +342 -5
  18. package/dist-plugin/{chunk-Y5CNFSSV.js → chunk-RVE3MNKM.js} +1 -1
  19. package/dist-plugin/{chunk-OPYOK2CA.js → chunk-ZHPX7ZYQ.js} +39 -1
  20. package/dist-plugin/{dataPaths-CRfD1LdA.d.ts → dataPaths-DZUzVv8H.d.cts} +3 -3
  21. package/dist-plugin/{dataPaths-CRfD1LdA.d.cts → dataPaths-DZUzVv8H.d.ts} +3 -3
  22. package/dist-plugin/fontPairing/index.cjs +32 -8
  23. package/dist-plugin/fontPairing/index.d.cts +1 -1
  24. package/dist-plugin/fontPairing/index.d.ts +1 -1
  25. package/dist-plugin/fontPairing/index.js +32 -8
  26. package/dist-plugin/generateColorsAndType/index.cjs +1 -1
  27. package/dist-plugin/generateColorsAndType/index.d.cts +1 -1
  28. package/dist-plugin/generateColorsAndType/index.d.ts +1 -1
  29. package/dist-plugin/generateColorsAndType/index.js +1 -1
  30. package/dist-plugin/index.cjs +417 -34
  31. package/dist-plugin/index.d.cts +1 -1
  32. package/dist-plugin/index.d.ts +1 -1
  33. package/dist-plugin/index.js +31 -23
  34. package/dist-plugin/migrateData/index.cjs +350 -9
  35. package/dist-plugin/migrateData/index.d.cts +1 -1
  36. package/dist-plugin/migrateData/index.d.ts +1 -1
  37. package/dist-plugin/migrateData/index.js +9 -5
  38. package/dist-plugin/tokensCssMigrations/index.cjs +39 -1
  39. package/dist-plugin/tokensCssMigrations/index.d.cts +1 -1
  40. package/dist-plugin/tokensCssMigrations/index.d.ts +1 -1
  41. package/dist-plugin/tokensCssMigrations/index.js +2 -2
  42. package/package.json +9 -2
  43. package/src/app/site.css +16 -0
  44. package/src/editor/core/fonts/googleFontsUrl.ts +44 -8
  45. package/src/editor/core/preview/lookPreview.ts +46 -5
  46. package/src/editor/core/productionPulse.ts +6 -2
  47. package/src/editor/core/sketch/index.ts +60 -0
  48. package/src/editor/core/sketch/maskField.ts +5 -5
  49. package/src/editor/core/sketch/sketchLayer.ts +19 -8
  50. package/src/editor/core/sketch/sketchStore.ts +238 -85
  51. package/src/editor/core/sketch/{sketchPresetService.ts → sketchStyleService.ts} +14 -14
  52. package/src/editor/core/sketch/{sketchPresets.ts → sketchStyles.ts} +17 -17
  53. package/src/editor/core/themes/themeDocumentSync.ts +2 -0
  54. package/src/editor/core/themes/themeInit.ts +19 -1
  55. package/src/editor/core/themes/themeService.ts +7 -2
  56. package/src/editor/core/themes/themeTypes.ts +5 -0
  57. package/src/editor/docs/content/editing-tokens.md +1 -1
  58. package/src/editor/docs/content/sketch-mode.md +27 -16
  59. package/src/editor/docs/content/themes-workflow.md +30 -21
  60. package/src/editor/docs/content/where-themes-live.md +9 -5
  61. package/src/editor/docs/content.generated.ts +4 -4
  62. package/src/editor/overlay/LiveTokensRouter.svelte +1 -1
  63. package/src/editor/ui/EditorViewSwitcher.svelte +3 -3
  64. package/src/editor/ui/FontStackEditor.svelte +49 -11
  65. package/src/editor/ui/ProjectFontsSection.svelte +6 -0
  66. package/src/editor/ui/ThemePanel.svelte +47 -1
  67. package/src/editor/ui/sections/textStyles.ts +15 -1
  68. package/src/editor/ui/sketch/SketchPreview.svelte +3 -3
  69. package/src/editor/ui/sketch/SketchTab.svelte +87 -53
  70. package/src/system/components/FloatingTokenTags.css +10 -8
  71. package/src/system/styles/tokens.css +17 -2
@@ -58,6 +58,7 @@
58
58
  markComponentSaved,
59
59
  } from '../core/store/editorStore';
60
60
  import { openThemeSlug } from '../core/store/editorConfigStore';
61
+ import { sketchOffLook, themeSketchStyle } from '../core/sketch/sketchStore';
61
62
  import { editorView } from '../core/store/editorViewStore';
62
63
  import {
63
64
  componentActiveRevision,
@@ -83,6 +84,9 @@
83
84
  let { showComponentsLink = true }: Props = $props();
84
85
 
85
86
  let canOpenComponents = $derived(showComponentsLink && $editorView !== 'components');
87
+ // showComponentsLink=false marks the ThemePanel that renders outside the
88
+ // view switcher (ComponentEditorPage), where neither pill can navigate.
89
+ let canOpenSketchStyle = $derived(showComponentsLink && $editorView !== 'sketch');
86
90
 
87
91
  let files: ThemeMeta[] = $state([]);
88
92
  let colorsFiles: ColorsAndTypeMeta[] = $state([]);
@@ -104,7 +108,7 @@
104
108
  let dirtyComponentCount = $derived(
105
109
  Object.values($componentDirty).filter(Boolean).length,
106
110
  );
107
- let unsavedEdits = $derived($colorsAndTypeDirty || dirtyComponentCount > 0);
111
+ let unsavedEdits = $derived($colorsAndTypeDirty || dirtyComponentCount > 0 || $sketchOffLook);
108
112
 
109
113
  async function refreshFiles() {
110
114
  try {
@@ -120,6 +124,10 @@
120
124
  if (active) {
121
125
  openThemeSlug.set(active._fileName ?? 'default');
122
126
  currentDisplayName = active.name ?? $openThemeSlug;
127
+ // Deleting the open theme repoints active without going through Load
128
+ // or Save, both of which already set this; this is the path that
129
+ // makes sketchStore's "set by every path that opens a theme" true.
130
+ themeSketchStyle.set(active.sketchStyle);
123
131
  }
124
132
  } catch {
125
133
  // silent
@@ -177,6 +185,10 @@
177
185
  editorView.set('components');
178
186
  }
179
187
 
188
+ function openSketchStyle() {
189
+ editorView.set('sketch');
190
+ }
191
+
180
192
  onMount(async () => {
181
193
  await refreshFiles();
182
194
  await refreshActive();
@@ -851,6 +863,40 @@
851
863
  </UIPillButton>
852
864
  {/if}
853
865
  </div>
866
+
867
+ <div class="part-head part-static">
868
+ <span class="part-label">Sketchstyle</span>
869
+ <span class="part-summary">
870
+ <span class="part-summary-sep">·</span>
871
+ {#if $sketchOffLook}
872
+ <span>off the theme</span>
873
+ {:else if $themeSketchStyle}
874
+ <span class="part-summary-text">{$themeSketchStyle.label}</span>
875
+ {:else}
876
+ <span>none</span>
877
+ {/if}
878
+ </span>
879
+ {#if canOpenSketchStyle}
880
+ <UIPillButton
881
+ size="compact"
882
+ icon="fa-pen-nib"
883
+ title="Open the Sketchstyle view"
884
+ onclick={openSketchStyle}
885
+ >
886
+ Open
887
+ </UIPillButton>
888
+ {/if}
889
+ <UIInfoPopover title="Sketchstyle" ariaLabel="About the sketchstyle">
890
+ <p>
891
+ <strong>Sketchstyle</strong> is part of the theme, the same way colors and type are.
892
+ It travels with the theme when you save, load or share it.
893
+ </p>
894
+ <p>
895
+ It does not reach a production build. Adopt publishes the crisp look; the drawing
896
+ stays a preview.
897
+ </p>
898
+ </UIInfoPopover>
899
+ </div>
854
900
  </div>
855
901
  </div>
856
902
 
@@ -64,6 +64,20 @@ export const TEXT_STYLES: TextStyle[] = [
64
64
  defaultElement: 'small',
65
65
  preview: 'Captions and secondary text.',
66
66
  },
67
+ {
68
+ name: 'editorial-xl',
69
+ label: 'Editorial XL',
70
+ prefix: '--editorial-xl',
71
+ defaultElement: 'editorial-xl',
72
+ preview: 'Pull quotes and opening statements.',
73
+ },
74
+ {
75
+ name: 'editorial-lg',
76
+ label: 'Editorial LG',
77
+ prefix: '--editorial-lg',
78
+ defaultElement: 'editorial-lg',
79
+ preview: 'Standfirsts and section ledes.',
80
+ },
67
81
  {
68
82
  name: 'editorial-md',
69
83
  label: 'Editorial MD',
@@ -76,7 +90,7 @@ export const TEXT_STYLES: TextStyle[] = [
76
90
  label: 'Editorial SM',
77
91
  prefix: '--editorial-sm',
78
92
  defaultElement: 'editorial-sm',
79
- preview: 'Standfirsts, pull quotes, and asides.',
93
+ preview: 'Captions, credits, and asides.',
80
94
  },
81
95
  {
82
96
  name: 'code',
@@ -6,10 +6,10 @@
6
6
  import Callout from '../../../system/components/Callout.svelte';
7
7
  import Toggle from '../../../system/components/Toggle.svelte';
8
8
  import { setSketchScope } from '../../core/sketch/sketchLayer';
9
- import type { SketchSettings } from '../../core/sketch/sketchPresets';
9
+ import type { SketchStyle } from '../../core/sketch/sketchStyles';
10
10
 
11
11
  interface Props {
12
- settings: SketchSettings;
12
+ settings: SketchStyle;
13
13
  enabled: boolean;
14
14
  }
15
15
 
@@ -42,7 +42,7 @@
42
42
  <p>Fill and outline displace on different noise seeds, so they disagree at the edges.</p>
43
43
  </Card>
44
44
  <Card title="Second card">
45
- <p>Same size and same preset. A different seed keeps the silhouette from repeating.</p>
45
+ <p>Same size and same sketchstyle. A different seed keeps the silhouette from repeating.</p>
46
46
  </Card>
47
47
  </div>
48
48
  </section>
@@ -10,20 +10,23 @@
10
10
  import UIPillButton from '../UIPillButton.svelte';
11
11
  import UIReveal, { REVEAL_MS } from '../UIReveal.svelte';
12
12
  import { scrollSectionIntoView } from '../scrollSection';
13
- import { SKETCH_PRESETS } from '../../core/sketch/sketchPresets';
13
+ import { SKETCH_STYLES } from '../../core/sketch/sketchStyles';
14
+ import { openThemeSlug } from '../../core/store/editorConfigStore';
14
15
  import {
15
16
  sketchEnabled,
16
- sketchPreset,
17
+ setSketchEnabled,
18
+ sketchOffLook,
19
+ sketchStyleName,
17
20
  sketchDirty,
18
21
  sketchSettings,
19
- selectSketchPreset,
22
+ selectSketchStyle,
20
23
  updateSketchSettings,
21
- userSketchPresets,
22
- refreshUserPresets,
23
- selectUserSketchPreset,
24
- saveCurrentAsSketchPreset,
25
- deleteUserSketchPreset,
26
- USER_PRESET_PREFIX,
24
+ savedSketchStyles,
25
+ refreshSavedSketchStyles,
26
+ selectSavedSketchStyle,
27
+ saveCurrentSketchStyle,
28
+ deleteSavedSketchStyle,
29
+ USER_STYLE_PREFIX,
27
30
  } from '../../core/sketch/sketchStore';
28
31
 
29
32
  interface Props {
@@ -37,6 +40,11 @@
37
40
 
38
41
  let s = $derived($sketchSettings);
39
42
 
43
+ // Save is disabled for the protected Default theme (ThemePanel.svelte);
44
+ // Save As is the gesture actually on offer there, so the off-look copy has
45
+ // to name it instead.
46
+ let activeIsProtected = $derived($openThemeSlug === 'default');
47
+
40
48
  type SectionKey = 'border' | 'fill' | 'shape' | 'icons' | 'noise';
41
49
 
42
50
  const SECTION_KEYS: SectionKey[] = ['border', 'fill', 'shape', 'icons', 'noise'];
@@ -81,8 +89,8 @@
81
89
  const ROUGHNESS = ['smooth', 'grainy', 'rough'] as const;
82
90
 
83
91
  /** A layer's own wavelength: the multiple, and the length it resolves to
84
- against the shared wave. The ratio is what a preset carries; the pixels
85
- are what you can picture. */
92
+ against the shared wave. The ratio is what a sketchstyle carries; the
93
+ pixels are what you can picture. */
86
94
  const against = (multiple: number) => `${Math.round(s.wobble * multiple)}px`;
87
95
 
88
96
  /** The wave's shape, named rather than numbered: 1 is the field as the filter
@@ -110,52 +118,52 @@
110
118
  + (s.waveform > 1 ? ` · ${waveformLabel(s.waveform)}` : ''),
111
119
  });
112
120
 
113
- /** Shipped and saved presets are one choice, so they share one radio group:
114
- picking a saved one clears the shipped selection natively. */
115
- const PRESET_RADIO_GROUP = 'sketch-preset';
121
+ /** Shipped and saved sketchstyles are one choice, so they share one radio
122
+ group: picking a saved one clears the shipped selection natively. */
123
+ const STYLE_RADIO_GROUP = 'sketchstyle';
116
124
 
117
125
  /** What the readout under the grid says. The selection survives a dial move,
118
126
  so the base keeps its name here and the blurb reports the drift instead. */
119
127
  let active = $derived(
120
128
  $sketchDirty
121
129
  ? { name: s.label, blurb: `Modified from ${s.label}. Save it to keep it.` }
122
- : { name: s.label, blurb: s.blurb || 'Your saved preset.' },
130
+ : { name: s.label, blurb: s.blurb || 'Your saved sketchstyle.' },
123
131
  );
124
132
 
125
133
  let naming = $state(false);
126
134
  let draftName = $state('');
127
- let presetError = $state('');
135
+ let styleError = $state('');
128
136
 
129
137
  onMount(() => {
130
- // No dev plugin (a built preview, say) means no saved presets. That is a
131
- // missing door, not a fault worth reporting in the tab.
132
- refreshUserPresets().catch(() => {});
138
+ // No dev plugin (a built preview, say) means no saved sketchstyles. That
139
+ // is a missing door, not a fault worth reporting in the tab.
140
+ refreshSavedSketchStyles().catch(() => {});
133
141
  });
134
142
 
135
143
  function startNaming() {
136
- draftName = s.label && $sketchPreset.startsWith(USER_PRESET_PREFIX) ? s.label : '';
137
- presetError = '';
144
+ draftName = s.label && $sketchStyleName.startsWith(USER_STYLE_PREFIX) ? s.label : '';
145
+ styleError = '';
138
146
  naming = true;
139
147
  }
140
148
 
141
149
  async function commitSave(e: SubmitEvent) {
142
150
  e.preventDefault();
143
151
  try {
144
- await saveCurrentAsSketchPreset(draftName);
152
+ await saveCurrentSketchStyle(draftName);
145
153
  naming = false;
146
154
  draftName = '';
147
- presetError = '';
155
+ styleError = '';
148
156
  } catch (err) {
149
- presetError = err instanceof Error ? err.message : 'Save failed';
157
+ styleError = err instanceof Error ? err.message : 'Save failed';
150
158
  }
151
159
  }
152
160
 
153
- async function runPresetAction(action: Promise<unknown>) {
161
+ async function runStyleAction(action: Promise<unknown>) {
154
162
  try {
155
163
  await action;
156
- presetError = '';
164
+ styleError = '';
157
165
  } catch (err) {
158
- presetError = err instanceof Error ? err.message : 'That did not work';
166
+ styleError = err instanceof Error ? err.message : 'That did not work';
159
167
  }
160
168
  }
161
169
 
@@ -229,19 +237,20 @@
229
237
  <div class="sketch-tab">
230
238
  <header class="head">
231
239
  <div class="head-row">
232
- <h2 class="section-title">Sketch</h2>
240
+ <h2 class="section-title">Sketchstyle</h2>
233
241
  <div class="switch">
234
242
  <Toggle
235
243
  label="Sketch mode"
236
244
  labelFirst
237
245
  checked={$sketchEnabled}
238
- onchange={(v) => sketchEnabled.set(v)}
246
+ onchange={setSketchEnabled}
239
247
  />
240
248
  <UIInfoPopover title="Sketch mode">
241
249
  <p>
242
- An effect layer over the active theme. It reads no theme values and writes none:
243
- each component's fill and outline are redrawn from the tokens that component
244
- already owns, then pushed around a shared noise field.
250
+ An effect layer over the active theme. It repaints each component's fill and
251
+ outline from the tokens that component already owns, then pushes them around a
252
+ shared noise field. Save in the Theme panel folds the dials into the open theme,
253
+ or Save As when the open theme is Motion Proto, the read-only built-in one.
245
254
  </p>
246
255
  <p>
247
256
  While it is on, the effect applies to the page behind this editor as well as to the
@@ -255,28 +264,46 @@
255
264
  <div class="readout" aria-live="polite">
256
265
  <span class="readout-name">{active.name}</span>
257
266
  <p class="readout-blurb">{active.blurb}</p>
267
+ {#if $sketchOffLook}
268
+ {#if $sketchEnabled}
269
+ <p class="readout-off-look">
270
+ {#if activeIsProtected}
271
+ Motion Proto is read-only, and these dials are ahead of it. Use Save As in the
272
+ Theme panel to keep them in a new theme.
273
+ {:else}
274
+ These dials are ahead of the saved theme. Save it in the Theme panel to fold them
275
+ in.
276
+ {/if}
277
+ </p>
278
+ {:else}
279
+ <p class="readout-off-look">
280
+ The theme carries a sketchstyle this page is not painting. Saving now would drop
281
+ it.
282
+ </p>
283
+ {/if}
284
+ {/if}
258
285
  </div>
259
286
 
260
287
  <div class="presets">
261
- {#each Object.entries(SKETCH_PRESETS) as [name, preset] (name)}
288
+ {#each Object.entries(SKETCH_STYLES) as [name, style] (name)}
262
289
  <label class="preset">
263
290
  <input
264
291
  type="radio"
265
- name={PRESET_RADIO_GROUP}
292
+ name={STYLE_RADIO_GROUP}
266
293
  value={name}
267
- checked={$sketchPreset === name}
268
- onchange={() => selectSketchPreset(name)}
294
+ checked={$sketchStyleName === name}
295
+ onchange={() => selectSketchStyle(name)}
269
296
  />
270
- <span class="preset-name">{preset.label}</span>
297
+ <span class="preset-name">{style.label}</span>
271
298
  </label>
272
299
  {/each}
273
300
  </div>
274
301
 
275
302
  <div class="saved">
276
303
  <div class="saved-head">
277
- <span class="band-label">Saved</span>
304
+ <span class="band-label">Saved sketchstyles</span>
278
305
  <UIPillButton size="compact" icon="fa-floppy-disk" onclick={startNaming}>
279
- Save current
306
+ Save as sketchstyle…
280
307
  </UIPillButton>
281
308
  </div>
282
309
 
@@ -286,8 +313,8 @@
286
313
  <input
287
314
  class="save-name"
288
315
  bind:value={draftName}
289
- placeholder="Preset name"
290
- aria-label="Preset name"
316
+ placeholder="Sketchstyle name"
317
+ aria-label="Sketchstyle name"
291
318
  autofocus
292
319
  />
293
320
  <UIPillButton size="compact" variant="primary" type="submit">Save</UIPillButton>
@@ -295,17 +322,17 @@
295
322
  </form>
296
323
  {/if}
297
324
 
298
- {#if $userSketchPresets.length > 0}
325
+ {#if $savedSketchStyles.length > 0}
299
326
  <div class="presets">
300
- {#each $userSketchPresets as saved (saved.fileName)}
327
+ {#each $savedSketchStyles as saved (saved.fileName)}
301
328
  <div class="saved-item">
302
329
  <label class="preset">
303
330
  <input
304
331
  type="radio"
305
- name={PRESET_RADIO_GROUP}
306
- value={USER_PRESET_PREFIX + saved.fileName}
307
- checked={$sketchPreset === USER_PRESET_PREFIX + saved.fileName}
308
- onchange={() => runPresetAction(selectUserSketchPreset(saved.fileName))}
332
+ name={STYLE_RADIO_GROUP}
333
+ value={USER_STYLE_PREFIX + saved.fileName}
334
+ checked={$sketchStyleName === USER_STYLE_PREFIX + saved.fileName}
335
+ onchange={() => runStyleAction(selectSavedSketchStyle(saved.fileName))}
309
336
  />
310
337
  <span class="preset-name">{saved.name}</span>
311
338
  </label>
@@ -314,15 +341,15 @@
314
341
  class="saved-delete"
315
342
  title="Delete {saved.name}"
316
343
  aria-label="Delete {saved.name}"
317
- onclick={() => runPresetAction(deleteUserSketchPreset(saved.fileName))}
344
+ onclick={() => runStyleAction(deleteSavedSketchStyle(saved.fileName))}
318
345
  ><i class="fas fa-xmark"></i></button>
319
346
  </div>
320
347
  {/each}
321
348
  </div>
322
349
  {/if}
323
350
 
324
- {#if presetError}
325
- <p class="saved-error">{presetError}</p>
351
+ {#if styleError}
352
+ <p class="saved-error">{styleError}</p>
326
353
  {/if}
327
354
  </div>
328
355
  </div>
@@ -793,9 +820,9 @@
793
820
  }
794
821
 
795
822
 
796
- /* Grid, saved band and readout are one control: which preset is on, and what
797
- that preset is. Splitting them left the description reading as a stray
798
- caption under the tab. */
823
+ /* Grid, saved band and readout are one control: which sketchstyle is on, and
824
+ what that sketchstyle is. Splitting them left the description reading as a
825
+ stray caption under the tab. */
799
826
  .picker {
800
827
  display: flex;
801
828
  flex-direction: column;
@@ -988,6 +1015,13 @@
988
1015
  color: var(--ui-text-secondary);
989
1016
  }
990
1017
 
1018
+ .readout-off-look {
1019
+ margin: var(--ui-space-2) 0 0;
1020
+ font-size: var(--ui-font-size-sm);
1021
+ font-weight: var(--ui-font-weight-medium);
1022
+ color: var(--ui-text-primary);
1023
+ }
1024
+
991
1025
  .dormant {
992
1026
  margin: 0;
993
1027
  font-size: var(--ui-font-size-sm);
@@ -3,12 +3,14 @@
3
3
  * animations are hardcoded so token edits don't repaint our chrome. The
4
4
  * exceptions are intentional and ARE the demo: the central box's
5
5
  * surface/radius/border (set inline), the dropdown (rendered through
6
- * <MenuSelect>), and elevation, which reads from the --shadow-* scale.
6
+ * <MenuSelect>), elevation, which reads from the --shadow-* scale, and the
7
+ * zap colour, which tracks --text-accent so the beam stays on-theme.
7
8
  * File is global; keep selectors `ftt-` prefixed.
8
9
  */
9
10
 
10
11
  .ftt-stage {
11
12
  --ftt-contrast-color: var(--color-white);
13
+ --ftt-zap-color: var(--text-accent);
12
14
  --ftt-string-opacity: 0.5;
13
15
  position: relative;
14
16
  width: 100%;
@@ -85,7 +87,7 @@
85
87
  }
86
88
 
87
89
  .ftt-string.ftt-live {
88
- stroke: #ff8eeb;
90
+ stroke: var(--ftt-zap-color);
89
91
  opacity: 1;
90
92
  }
91
93
 
@@ -103,18 +105,18 @@
103
105
  }
104
106
 
105
107
  /* --- Energy ball ---------------------------------------------------------- */
106
- /* Colour must match `.ftt-string.ftt-live` so the beam reads as a continuous
107
- trail, not a colour-shifting projectile. */
108
+ /* Shares --ftt-zap-color with `.ftt-string.ftt-live` so the beam reads as a
109
+ continuous trail, not a colour-shifting projectile. */
108
110
  .ftt-energy-ball {
109
111
  position: absolute;
110
112
  width: 0.875rem;
111
113
  height: 0.875rem;
112
114
  translate: -50% -50%;
113
115
  border-radius: 9999px;
114
- background: #ff8eeb;
116
+ background: var(--ftt-zap-color);
115
117
  box-shadow:
116
- 0 0 8px 2px #ff8eeb,
117
- 0 0 18px 4px #ff8eeb;
118
+ 0 0 8px 2px var(--ftt-zap-color),
119
+ 0 0 18px 4px var(--ftt-zap-color);
118
120
  pointer-events: none;
119
121
  opacity: 0;
120
122
  z-index: 6;
@@ -233,7 +235,7 @@
233
235
  z-index: 30;
234
236
  }
235
237
  .ftt-tag-trigger:focus-visible {
236
- outline: 2px solid #f31fdb;
238
+ outline: 2px solid var(--ftt-zap-color);
237
239
  outline-offset: 3px;
238
240
  border-radius: 9999px;
239
241
  }
@@ -681,8 +681,23 @@
681
681
  --code-line-height: var(--line-height-normal);
682
682
  --code-letter-spacing: var(--letter-spacing-normal);
683
683
 
684
- /* Editorial — the long-reading role. Each step mirrors its body counterpart
685
- until the stack is repointed. */
684
+ /* Editorial — the long-reading role. `md` is the reading step and takes the
685
+ most open leading; every step away from it tightens by one, so a lede or a
686
+ pull quote is not set at reading leading. Sizes stay inside the xs–xl band
687
+ the responsive scale leaves constant, so an article does not resize itself
688
+ between breakpoints. */
689
+ --editorial-xl-font-family: var(--font-editorial);
690
+ --editorial-xl-font-size: var(--font-size-xl);
691
+ --editorial-xl-font-weight: var(--font-weight-normal);
692
+ --editorial-xl-line-height: var(--line-height-tighter);
693
+ --editorial-xl-letter-spacing: var(--letter-spacing-normal);
694
+
695
+ --editorial-lg-font-family: var(--font-editorial);
696
+ --editorial-lg-font-size: var(--font-size-lg);
697
+ --editorial-lg-font-weight: var(--font-weight-normal);
698
+ --editorial-lg-line-height: var(--line-height-tight);
699
+ --editorial-lg-letter-spacing: var(--letter-spacing-normal);
700
+
686
701
  --editorial-md-font-family: var(--font-editorial);
687
702
  --editorial-md-font-size: var(--font-size-md);
688
703
  --editorial-md-font-weight: var(--font-weight-normal);