@mnstry/atelier 0.2.0-alpha.15 → 0.2.0-alpha.17

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 (33) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -1
  3. package/docs/guides/extensions.md +6 -6
  4. package/docs/guides/graph-workspace.md +19 -7
  5. package/docs/guides/upgrade.md +24 -0
  6. package/docs/reference/graph-workspace.md +4 -4
  7. package/docs/reference/portable-presentation.md +9 -0
  8. package/docs/reference/ui-primitives.md +116 -0
  9. package/docs/start/independent-start.md +2 -2
  10. package/fixtures/templates/local-library.v1.json +3 -1
  11. package/package.json +3 -1
  12. package/src/extensions/project.mjs +5 -5
  13. package/src/graph-workspace/browser/index.mjs +1 -1
  14. package/src/graph-workspace/browser/{constellation.mjs → map.mjs} +7 -2
  15. package/src/graph-workspace/browser/styles.mjs +1 -1
  16. package/src/graph-workspace/browser/workbench.mjs +2 -2
  17. package/src/graph-workspace/browser/workspace.mjs +53 -51
  18. package/src/graph-workspace/disclosure.mjs +1 -1
  19. package/src/graph-workspace/geometry.mjs +1 -1
  20. package/src/graph-workspace/index.mjs +2 -2
  21. package/src/graph-workspace/recipes.mjs +24 -12
  22. package/src/graph-workspace/scale.mjs +5 -2
  23. package/src/graph-workspace/theme.mjs +2 -2
  24. package/src/ui/primitives/browser.mjs +6 -0
  25. package/src/ui/primitives/carousel.mjs +551 -0
  26. package/src/ui/primitives/disclosure.mjs +50 -0
  27. package/src/ui/primitives/index.mjs +7 -0
  28. package/src/ui/primitives/motion.mjs +50 -0
  29. package/src/ui/primitives/render.mjs +22 -0
  30. package/src/ui/primitives/scroll-lock.mjs +47 -0
  31. package/src/ui/primitives/state.mjs +130 -0
  32. package/src/ui/primitives/styles.mjs +127 -0
  33. package/templates/external-project-workspace/pack-lifecycle.json +3 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0-alpha.17
4
+
5
+ - Add UI primitives (`@mnstry/atelier/ui/primitives` and `/ui/primitives/browser`), the shared behaviour layer beside the control system. The carousel combines the Urana Heart site's carousel (seamless loops over inert clones, horizontal drag intent, flick steps, focus kept off inert slides, page dots and thumbnails, a polite live status, height that follows the slide, receding neighbours and a still row when every card fits, and a crossfade mode) with Botanika's additions (a draggable scroll bar, capped momentum for multi-card strips, page stepping that never reads as backwards in a short loop, and discrete or smooth autoplay with a pause control that holds on hover, focus, touch, wheel, drag, a hidden tab or off screen and never runs under reduced motion). A `visible` selection mode keeps every on-screen card interactive for product strips. Also: interruptible `<details>` disclosures, the counted layout-stable scroll lock from the portable presentation contract (measured from the body's laid-out width, so it holds with `scrollbar-gutter: stable`), and one motion owner. Pure state is exported for Node; tenants theme through `--system-carousel-*`. See the [UI primitives reference](docs/reference/ui-primitives.md).
6
+
7
+ ## 0.2.0-alpha.16
8
+
9
+ - Rename the graph workspace's room from constellation to **map** and its middle layer from coach to **distribution**. The runtime already holds *constellation* as an identity canonical (a grouping of people), and Atelier already defines the distribution tier. New names: `ROOMS` is `map`, `workbench`, `review`; `LAYERS` is `atelier`, `distribution`, `owner`; `planMap`, `createMap`, the shell handle's `map` and `backToMap`, `setBy: 'distribution'`; interface text says "map". The alpha.15 names keep working for this release and are removed in the next: `room: 'constellation'` (listed in the new `DEPRECATED_ROOMS`), `composeRecipes({ coach })` (giving both `coach` and `distribution` refuses), `planConstellation`, `createConstellation`, the handle's `constellation` and `backToConstellation`, and the element's `constellation` class.
10
+
3
11
  ## 0.2.0-alpha.15
4
12
 
5
13
  - Patch the fast-uri dependency and override from 3.1.8 to 3.1.9, the patched 3.x release for GHSA-w74x-v6f9-j3xg, GHSA-xhq7-xmqj-769h, GHSA-wjr9-j795-jxcr and GHSA-333x-w4r6-6crp (3.1.8 is in each affected range; whether Atelier's use was exploitable is not established). Ajv's own `^3.0.1` range resolves to the same copy, so a bare consumer installs 3.1.9 only, and `npm audit` reports no runtime advisory.
@@ -312,6 +320,7 @@
312
320
 
313
321
  ## Unreleased
314
322
 
323
+ - Document how a host locks page scrolling around the presentation's modal confirmation without shifting the layout (pay the hidden scrollbar back as body end padding, count locks), and test that the browser binding itself never changes the host page's overflow, padding or scroll position.
315
324
  - Patch the fast-uri dependency and override from 3.1.7 to 3.1.8, the patched
316
325
  3.x release for GHSA-hrr3-gc8f-f4qj (3.1.7 is in the affected range; whether
317
326
  Atelier's use was exploitable is not established). The direct dependency
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MNSTRY Atelier
2
2
 
3
- `0.2.0-alpha.15` adds a [graph workspace](docs/guides/graph-workspace.md) (a constellation of every readable record, a focused authoring workbench and a review room, built from primitives coaches and owners can compose), renderer-neutral graph views and an [extension SDK and Postgres project workflow](docs/guides/extensions.md). Native host qualification remains a separate gate.
3
+ `0.2.0-alpha.17` adds shared [UI primitives](docs/reference/ui-primitives.md): one carousel, disclosure, scroll lock and motion layer for Atelier-built sites. `0.2.0-alpha.16` renames the [graph workspace](docs/guides/graph-workspace.md)'s room to the map and its middle layer to distribution, keeping the alpha.15 names for one release. `0.2.0-alpha.15` added the workspace (a map of every readable record, a focused authoring workbench and a review room, built from primitives distributions and owners can compose), renderer-neutral graph views and an [extension SDK and Postgres project workflow](docs/guides/extensions.md). Native host qualification remains a separate gate.
4
4
 
5
5
  **Turn a repository into a living, governed system.**
6
6
 
@@ -45,12 +45,12 @@ distributed in this package.
45
45
 
46
46
  The runtime parser is required after JSON Schema validation: it enforces unique IDs and cross-references, recovery scope/effect, current-schema readability, Unicode and total JSON bounds. Native Postgres operation results are capped at 60 KiB inside the transaction, leaving room for the original-operation recovery proof within the 64 KiB action output bound. Every failure after successful admission remains an uncertain operation; inspect its durable receipt before deciding what to do next.
47
47
 
48
- The candidate SDK version 0.2.0-alpha.15 is unpublished. Before its release,
49
- pack the reviewed Atelier checkout with `npm pack --ignore-scripts` and install
50
- the resulting local tarball in the generated project with
51
- `npm install /absolute/path/to/mnstry-atelier-0.2.0-alpha.15.tgz`. Then run
52
- `npm run typecheck` and reseal the reviewed project. Do not expect the unpublished
53
- version to install from the public registry. String length limits use Unicode
48
+ The generated project pins the SDK to the Atelier version that generated it.
49
+ When that version is published, `npm install` resolves it. From an unreleased
50
+ checkout, pack the reviewed checkout with `npm pack --ignore-scripts` and install
51
+ the resulting local tarball in the generated project by its absolute path. Then
52
+ run `npm run typecheck` and reseal the reviewed project. An unpublished version
53
+ does not install from the public registry. String length limits use Unicode
54
54
  code points, matching JSON Schema; ill-formed surrogate sequences remain refused.
55
55
 
56
56
  The source seal covers package.json, tsconfig.json, src/, present npm lockfiles,
@@ -3,18 +3,30 @@
3
3
  A graph workspace lets a person explore a whole knowledge graph, then shape its concepts one step at a time. It is built in three layers, and the last one belongs to the owner:
4
4
 
5
5
  1. **Atelier** ships primitives and defaults: the read model, scale and layout helpers, label placement, recipes, themes, authoring and the browser rooms.
6
- 2. **A coach** builds tooling for a cohort from those pieces: their own recipes, a theme, perhaps a different shell around the same rooms.
6
+ 2. **A distribution** adapts those pieces for a programme or cohort: its own recipes, a theme, perhaps a different shell around the same rooms. This is the same middle tier as an Atelier [distribution](distributions.md): add and rebrand, never alter root semantics.
7
7
  3. **The owner** shapes their own system from everything they receive: hiding what they do not use, renaming, adding their own recipes and appearance.
8
8
 
9
9
  Nothing in a layer is hard-coded into the one below it. Recipes and themes are data that compose by name, and every resolved recipe field records which layer set it.
10
10
 
11
11
  ## One graph, three rooms
12
12
 
13
- - **Constellation.** Every readable record, alive on a night-sky canvas (the default), with soft glows for the owner's folders and folder names at a distance. Names fade in by importance as you zoom in and never overlap each other or the controls. Hovering lights a neighbourhood; clicking selects without moving the map. "Let it move" turns on live physics.
13
+ - **Map.** Every readable record, alive on a night-sky canvas (the default), with soft glows for the owner's folders and folder names at a distance. Names fade in by importance as you zoom in and never overlap each other or the controls. Hovering lights a neighbourhood; clicking selects without moving the map. "Let it move" turns on live physics.
14
14
  - **Workbench.** A sparse set of rich cards (incoming on the left, outgoing on the right) beside a focused authoring sheet. One subject at a time, one step at a time, with deferral and structural guidance. Drafts belong to one workspace identity (and reader scope, where the host has one), so two workspaces never see each other's drafts. Without an identity they live in this tab only. With `profile.id` or `draftIdentity` they persist in this browser, and with a host storage port they persist there. The interface says which.
15
15
  - **Review.** Before and proposed, the state of the evidence, and that a person decides. Nothing is sent.
16
16
 
17
- A work **recipe** picks the room, the lens and the single primary action. Lenses (`question`, `concept`, `zone`) belong to the constellation, subjects (`record`, `concept`, `relation`) to the workbench, and the review room takes neither. The primary action is one of `search`, `open-evidence`, `next-step`, `walk`, `review` or `pick`, and only one the recipe's room can carry out (`primaryActionsFor`). Combinations the shell cannot honour are refused when recipes are composed, and recipes the workspace cannot run (a relation recipe with no relations, say) are not offered. Defaults: *Explore*, *Answer a question*, *Refine a concept*, *Inspect a record*. A command bar (⌘K or `/`) replaces persistent navigation. Text view lists every record, and without WebGL every task remains available through Text view and the workbench.
17
+ A work **recipe** picks the room, the lens and the single primary action. Lenses (`question`, `concept`, `zone`) belong to the map, subjects (`record`, `concept`, `relation`) to the workbench, and the review room takes neither. The primary action is one of `search`, `open-evidence`, `next-step`, `walk`, `review` or `pick`, and only one the recipe's room can carry out (`primaryActionsFor`). Combinations the shell cannot honour are refused when recipes are composed, and recipes the workspace cannot run (a relation recipe with no relations, say) are not offered. Defaults: *Explore*, *Answer a question*, *Refine a concept*, *Inspect a record*. A command bar (⌘K or `/`) replaces persistent navigation. Text view lists every record, and without WebGL every task remains available through Text view and the workbench.
18
+
19
+ ## Names from alpha.15
20
+
21
+ 0.2.0-alpha.16 renamed the room and the middle layer. The runtime already uses
22
+ *constellation* for a grouping of people, so a view of records is called the
23
+ **map**; the middle layer reuses Atelier's existing **distribution** tier. The
24
+ alpha.15 names keep working for one release and are then removed:
25
+ `room: 'constellation'` is read as `'map'`, `composeRecipes({ coach })` as
26
+ `{ distribution }` (giving both refuses), `planConstellation` and
27
+ `createConstellation` alias `planMap` and `createMap`, and the shell handle's
28
+ `constellation` and `backToConstellation` alias `map` and `backToMap`. The map
29
+ element carries both the `map` and `constellation` classes.
18
30
 
19
31
  ## Honesty rules the primitives keep
20
32
 
@@ -26,7 +38,7 @@ A work **recipe** picks the room, the lens and the single primary action. Lenses
26
38
 
27
39
  ## Scale
28
40
 
29
- `planConstellation()` decides whether to draw every record or to show zones. The default budget is 12,000 records or 60,000 links. Above it, `aggregateZones()` gives counts by zone and directed bundles between zones, and `zoneSlice()` opens one zone at a time. Every link touching the zone is accounted for exactly once (drawn, omitted inside the zone, to another zone, or omitted to another zone), and plan concepts list only the kept records that declare them, with the rest counted. Aggregating 30,000 records is part of the test suite. On a laptop GPU the constellation holds 60 fps at 5,000 records, with labels and zone glows.
41
+ `planMap()` decides whether to draw every record or to show zones. The default budget is 12,000 records or 60,000 links. Above it, `aggregateZones()` gives counts by zone and directed bundles between zones, and `zoneSlice()` opens one zone at a time. Every link touching the zone is accounted for exactly once (drawn, omitted inside the zone, to another zone, or omitted to another zone), and plan concepts list only the kept records that declare them, with the rest counted. Aggregating 30,000 records is part of the test suite. On a laptop GPU the constellation holds 60 fps at 5,000 records, with labels and zone glows.
30
42
 
31
43
  ## Build a workspace
32
44
 
@@ -43,8 +55,8 @@ import { mountGraphWorkspace } from '@mnstry/atelier/graph-workspace/browser'
43
55
  const model = buildWorkspaceModel({ graph, plan, zoneRule: { prefix: 'notes/', depth: 1 } })
44
56
  layoutWorkspace(model, { Graph, forceAtlas2 }) // or store positions at build time
45
57
 
46
- const recipes = composeRecipes({ coach: coachRecipes, owner: ownerRecipes }).recipes
47
- const theme = composeTheme(DEFAULT_THEME, coachTheme, ownerTheme) // every layer is checked; one bad layer refuses the theme
58
+ const recipes = composeRecipes({ distribution: distributionRecipes, owner: ownerRecipes }).recipes
59
+ const theme = composeTheme(DEFAULT_THEME, distributionTheme, ownerTheme) // every layer is checked; one bad layer refuses the theme
48
60
 
49
61
  mountGraphWorkspace(element, { model, recipes, theme, deps: { Sigma, Graph, EdgeArrowProgram, FA2Layout },
50
62
  profile: { id: 'workspace-id', name: 'My notebook', vocabulary: { record: 'note', records: 'notes', zone: 'folder' } },
@@ -57,7 +69,7 @@ Atelier does not depend on `sigma` or `graphology`; the host injects them. Pin s
57
69
 
58
70
  ## Keeping a shareable layer clean
59
71
 
60
- A coach or owner who builds on private workspaces can check that nothing from them reaches a shareable layer. `fingerprintsFrom()` derives distinctive strings from a private graph and plan (record ids, paths, long titles, summary openings, plan wording and names). `scanForFingerprints()` reports where any of them appears in shareable text, naming the file and a masked sample only.
72
+ A distribution or owner who builds on private workspaces can check that nothing from them reaches a shareable layer. `fingerprintsFrom()` derives distinctive strings from a private graph and plan (record ids, paths, long titles, summary openings, plan wording and names). `scanForFingerprints()` reports where any of them appears in shareable text, naming the file and a masked sample only.
61
73
 
62
74
  ## What this does not do
63
75
 
@@ -9,6 +9,30 @@ workspace between Atelier package releases. The flow is local-only: it does not
9
9
  provision repositories, contact a Git host, mutate the MNSTRY runtime, or write
10
10
  through a browser view.
11
11
 
12
+ ## Upgrading to 0.2.0-alpha.17
13
+
14
+ This release only adds `@mnstry/atelier/ui/primitives` and
15
+ `@mnstry/atelier/ui/primitives/browser`; nothing existing changes and no stored
16
+ record changes format. The alpha.15 graph workspace names that alpha.16 kept
17
+ for one release are still accepted here; their removal moves to a later release.
18
+
19
+ ## Upgrading to 0.2.0-alpha.16
20
+
21
+ This release renames two graph workspace names; nothing else changes and no
22
+ stored record changes format.
23
+
24
+ - The room `constellation` is now `map`, and the middle recipe and theme layer
25
+ `coach` is now `distribution`. Use `room: 'map'`,
26
+ `composeRecipes({ distribution })`, `planMap`, `createMap`, and the shell
27
+ handle's `map` and `backToMap`. Provenance now reads `setBy: 'distribution'`;
28
+ code that compared `setBy` values with `'coach'` must compare with
29
+ `'distribution'`.
30
+ - The alpha.15 names still work in this release and are removed in the next:
31
+ `room: 'constellation'`, `composeRecipes({ coach })` (do not pass both),
32
+ `planConstellation`, `createConstellation`, `constellation`,
33
+ `backToConstellation` and the `.constellation` class. Styles that target
34
+ `.atelier-gw .constellation` should move to `.atelier-gw .map`.
35
+
12
36
  ## Upgrading to 0.2.0-alpha.15
13
37
 
14
38
  This release adds the graph workspace (`@mnstry/atelier/graph-workspace` and
@@ -12,13 +12,13 @@ Two package subpaths. The first is pure and runs anywhere; the second needs a br
12
12
  | `neighbours(model, id)` | Neighbour ids in either direction. |
13
13
  | `seedPositions`, `layoutWorkspace(model, { Graph, forceAtlas2, seed?, iterations?, settings? })`, `normalizePositions`, `LAYOUT_WEIGHTS` | Deterministic zone-seeded ForceAtlas2 layout (injected), normalised to a 1,000-unit frame. |
14
14
  | `zoneGeometry(model, { cell? })` | Density blobs and a label anchor per zone. |
15
- | `planConstellation(model, budget?)`, `DEFAULT_BUDGET` | `records` or `zones` mode for the whole view. |
15
+ | `planMap(model, budget?)`, `DEFAULT_BUDGET` | `records` or `zones` mode for the whole view. `planConstellation` is its deprecated alpha.15 name. |
16
16
  | `aggregateZones(model)` | `atelier.graph-workspace-zones@v1`: counts by zone and type, directed bundles by edge class, coverage. |
17
17
  | `zoneSlice(model, zoneId, { limit })` | Bounded sub-model. `slice` reports `linksTouchingZone` = drawn + `omittedLinksInsideZone` + `linksToOtherZonesCount` + `omittedLinksToOtherZones`, with `linksToOtherZones` per zone. `coverage.byClass` counts the drawn links. Plan concepts keep only kept declaring records, with `omittedDeclarations`. |
18
18
  | `rankRecords(model)` | Records by connection count, then title. |
19
19
  | `placeLabels({ candidates, measure, width, height, reserved?, budget?, threshold? })` | Collision-free label placement with four slots per label. Pure. |
20
20
  | `labelThreshold(ratio, boost?)`, `labelBudget(width, height, ratio, boost?)`, `overlaps` | Defaults for zoom-dependent label density. |
21
- | `DEFAULT_RECIPES`, `composeRecipes({ atelier?, coach?, owner? })`, `validateRecipe`, `availableRecipes(recipes, model)` | Layered recipes with per-field provenance (`setBy`). Rooms: `constellation`, `workbench`, `review`; lenses (constellation only): `question`, `concept`, `zone`; subjects (workbench only, required there): `record`, `concept`, `relation`; primary actions (`PRIMARY_ACTIONS`): `search`, `open-evidence`, `next-step`, `walk`, `review`, `pick`, of which `primaryActionsFor(recipe)` names those the shell honours in a recipe's room (review: `review`; workbench: `next-step`, or `walk` for a record; zone lens: `pick`, `search`; question or concept lens: `open-evidence`, `pick`, `search`; no lens: `search`). `composeRecipes` throws on any other combination; `availableRecipes` drops recipes the model cannot run (concept, relation or question work without them in the plan, a review without concepts, a zone lens without zones, a record subject without records). |
21
+ | `DEFAULT_RECIPES`, `composeRecipes({ atelier?, distribution?, owner? })`, `validateRecipe`, `availableRecipes(recipes, model)` | Layered recipes with per-field provenance (`setBy`). Rooms (`ROOMS`): `map`, `workbench`, `review`; layers (`LAYERS`): `atelier`, `distribution`, `owner`. The alpha.15 names `coach` (layer) and `constellation` (room, see `DEPRECATED_ROOMS` and `currentRoom(room)`) are accepted for one release and read as `distribution` and `map`, including in already composed recipes given to `mountGraphWorkspace`; passing both `coach` and `distribution` throws, and only an omitted middle layer defaults (`null` is refused). Lenses (map only): `question`, `concept`, `zone`; subjects (workbench only, required there): `record`, `concept`, `relation`; primary actions (`PRIMARY_ACTIONS`): `search`, `open-evidence`, `next-step`, `walk`, `review`, `pick`, of which `primaryActionsFor(recipe)` names those the shell honours in a recipe's room (review: `review`; workbench: `next-step`, or `walk` for a record; zone lens: `pick`, `search`; question or concept lens: `open-evidence`, `pick`, `search`; no lens: `search`). `composeRecipes` throws on any other combination; `availableRecipes` drops recipes the model cannot run (concept, relation or question work without them in the plan, a review without concepts, a zone lens without zones, a record subject without records). |
22
22
  | `DEFAULT_THEME`, `composeTheme(base, ...layers)`, `validateTheme`, `contrastRatio` | `atelier.graph-workspace-theme@v1`, night (dark) by default with a paper (light) scheme. Every layer is checked before and after composing. Each CSS custom property has a declared type (colour, font list, length, shadow, duration, easing), so URLs, `image-set()` and other resource loads are refused, as are unknown fields, schemes and tokens. `--canvas` must equal `graph.canvas`; interface text must reach 4.5:1 on the canvas and surfaces, and on `--surface-glass` composited over the canvas and `--surface-2`; labels need 7:1 and zone colours 3:1. |
23
23
  | `conceptSteps`, `relationSteps`, `guideConcept`, `guideRelation`, `stepState`, `readiness(plan, store, readableIds)` | Focused authoring steps and structural guidance. `readiness` needs the `Set` of record ids this reader can open; for concepts and relations alike, missing or unreadable evidence and a missing disconfirming case are errors, so `ready-for-human-review` means every step is complete and every citation is readable here. |
24
24
  | `createDraftStore({ storage, key, identity, persistence?, delayMs? })`, `memoryStorage()` | Drafts behind a storage port `{ getItem, setItem, removeItem }`, saved in an `atelier.graph-workspace-draft@v1` envelope bound to `identity` (`{ workspace, ...scope }`); a draft saved under another identity is never restored. `persistence` is `tab`, `browser` or `host`. Status: `empty`, `restored`, `saving`, `saved`, `unavailable`. |
@@ -28,8 +28,8 @@ Two package subpaths. The first is pure and runs anywhere; the second needs a br
28
28
 
29
29
  | Export | Purpose |
30
30
  | --- | --- |
31
- | `mountGraphWorkspace(element, { model, deps, recipes?, theme?, scheme?, profile?, storage?, draftIdentity?, persistence?, draftKey?, initialRecipe?, intro?, webgl? })` | Default shell with the three rooms, command bar, display menu, question, concept and zone lenses, the recipe's primary action, selection card and Text view. Drafts stay in the tab unless `draftIdentity` or `profile.id` names the workspace; a host `storage` port requires one. Returns `{ state, recipes, store, constellation, workbench, select, openWorkbench, openReview, setRecipe, runPrimary, backToConstellation, toggleText, setScheme, readiness, destroy }`. One per document. |
32
- | `createConstellation({ container, model, palette, deps, font?, reserveSelector?, onSelect?, onHover?, onOpen? })` | The constellation room alone. `deps = { Sigma, Graph, EdgeArrowProgram, FA2Layout? }`. Elements matching `reserveSelector` (default `[data-graph-reserve]`) are kept free of labels. |
31
+ | `mountGraphWorkspace(element, { model, deps, recipes?, theme?, scheme?, profile?, storage?, draftIdentity?, persistence?, draftKey?, initialRecipe?, intro?, webgl? })` | Default shell with the three rooms, command bar, display menu, question, concept and zone lenses, the recipe's primary action, selection card and Text view. Drafts stay in the tab unless `draftIdentity` or `profile.id` names the workspace; a host `storage` port requires one. Returns `{ state, recipes, store, map, workbench, select, openWorkbench, openReview, setRecipe, runPrimary, backToMap, toggleText, setScheme, readiness, destroy }`, plus the deprecated aliases `constellation` and `backToConstellation`. One per document. |
32
+ | `createMap({ container, model, palette, deps, font?, reserveSelector?, onSelect?, onHover?, onOpen? })` | The map room alone (`createConstellation` is its deprecated alpha.15 name). `deps = { Sigma, Graph, EdgeArrowProgram, FA2Layout? }`. Elements matching `reserveSelector` (default `[data-graph-reserve]`) are kept free of labels. |
33
33
  | `createWorkbench(...)`, `renderReview(...)` | The other two rooms, for custom shells. |
34
34
  | `applyTheme(theme, scheme, element)` | Validate and apply a theme scheme as custom properties; returns the graph palette. |
35
35
  | `GRAPH_WORKSPACE_CSS`, `injectGraphWorkspaceStyles(document)` | Styles scoped under `.atelier-gw`, responsive by container size. |
@@ -119,6 +119,15 @@ request port because native has no browser default. A web host with unsaved work
119
119
  must omit item hrefs until safe navigation is available through its existing
120
120
  route/draft authority; this module does not install an unsaved-work route guard.
121
121
  Do not infer identical web/native navigation interception from shared labels.
122
+
123
+ The confirmation is a native modal `<dialog>`. The binding never changes the
124
+ host document's overflow, padding or scroll position, so opening it cannot
125
+ shift the page. A host that also locks page scrolling while it is open owns that
126
+ lock and must not let it shift the layout: measure the scrollbar it is about to
127
+ hide (`innerWidth - documentElement.clientWidth`, zero for overlay scrollbars),
128
+ add that width to the body's existing end padding, expose it as a custom
129
+ property for fixed or sticky elements, and restore both when the last lock is
130
+ released. Count locks so one closing overlay cannot release another. `lockScroll` and `unlockScroll` in [UI primitives](ui-primitives.md) implement this lock.
122
131
  Media resolves only local paths; hosts must prevent
123
132
  those routes from redirecting outside their intended boundary. The model is not
124
133
  a network-security boundary for a consumer's asset server.
@@ -0,0 +1,116 @@
1
+ # UI primitives
2
+
3
+ `@mnstry/atelier/ui/primitives` is the shared behaviour layer for sites and apps
4
+ built with Atelier. It sits beside the control system (`@mnstry/atelier/ui`) and
5
+ follows the same rules: no tenant content, plain HTML contracts that work
6
+ without script, and theming only through `--system-*` custom properties.
7
+
8
+ | Import | Runs in | Provides |
9
+ | --- | --- | --- |
10
+ | `@mnstry/atelier/ui/primitives` | Node and browsers | Pure state functions, `carouselStyles`, `renderCarouselControls` |
11
+ | `@mnstry/atelier/ui/primitives/browser` | Browsers | `enhanceCarousels`, `enhanceDisclosures`, `lockScroll` and `unlockScroll`, the motion owner |
12
+
13
+ The carousel brings together the Urana Heart site's carousel (seamless loops,
14
+ drag intent, focus safety, page dots and thumbnails, live status, height that
15
+ follows the slide, receding neighbours, still rows) and Botanika's additions (a
16
+ draggable scroll bar, momentum for multi-card strips, autoplay, a pause control).
17
+
18
+ ## Carousel
19
+
20
+ ### Markup contract
21
+
22
+ ```html
23
+ <div class="system-carousel" data-carousel="track" data-select="visible" data-loop
24
+ data-autoplay="discrete" data-interval="6000"
25
+ role="region" aria-roledescription="carousel" aria-label="Featured collection">
26
+ <div data-carousel-viewport tabindex="0" aria-label="Featured collection, scroll horizontally">
27
+ <div data-carousel-track>
28
+ <article>…</article>
29
+ <article>…</article>
30
+ </div>
31
+ </div>
32
+ <!-- optional: page dots or thumbnails -->
33
+ <div class="system-carousel__pages">
34
+ <button type="button" class="system-carousel__page" data-page aria-label="Show slide 1">…</button>
35
+ </div>
36
+ <!-- controls and status: renderCarouselControls({ label, autoplay }) -->
37
+ </div>
38
+ ```
39
+
40
+ Without script, the viewport is a native horizontal scroller with snap points,
41
+ so touch, trackpad, wheel and keyboard scrolling all work. Slides are the
42
+ track's children; if any child carries `data-slide`, only those count.
43
+
44
+ ### Options (attributes on the root)
45
+
46
+ | Attribute | Values | Effect |
47
+ | --- | --- | --- |
48
+ | `data-carousel` | `track` (default), `fade` | Scrolling strip, or slides crossfading in one grid cell |
49
+ | `data-select` | `current` (default), `visible` | `current` makes every slide except the current one inert (slideshows); `visible` keeps every card on screen interactive (product strips) |
50
+ | `data-step` | `item`, `page` | What an arrow moves. Defaults to `page` for `visible` and `item` for `current`. A loop barely longer than a page steps by the hidden remainder |
51
+ | `data-loop` | present | Seamless loop over inert clones; arrows never stop |
52
+ | `data-peek` | present | Centre-aligned slides; neighbours recede (`--d` from 0 to 1) and clicking one centres it; a still row when every card fits at `data-row-min` (default 400px) |
53
+ | `data-fit` | `current` | The viewport height follows the slide in view, blending as slides pass |
54
+ | `data-autoplay` | `off` (default), `discrete`, `smooth` | Advance one page every `data-interval` ms (at least 2500), or drift continuously at `data-speed` px/s |
55
+
56
+ ### Behaviour
57
+
58
+ - **Mouse drag** on the slides: a drag starts only once horizontal movement
59
+ clearly dominates, so vertical page scrolling is never taken over. Clicks at
60
+ the end of a drag are swallowed; a plain click still follows its link.
61
+ Slideshows step one slide (distance or flick); `visible` strips glide with
62
+ capped momentum and settle on a card.
63
+ - **Scroll bar** (`[data-scrollbar]`): click to jump, or drag the thumb. It
64
+ covers one set of slides in a loop.
65
+ - **Arrows, pages and keys:** Previous, Next, page buttons, and Left, Right,
66
+ Home and End on the carousel. Reader-initiated moves are announced politely in
67
+ `[data-status]`; autoplay moves are not.
68
+ - **Autoplay** never runs under reduced motion. It holds while the pointer is
69
+ over the carousel, while focus is inside it, during a touch, wheel, drag or
70
+ scrub, while the tab is hidden and while the carousel is off screen. The
71
+ rendered toggle pauses and resumes it. `data-autoplay-state` reads `playing`,
72
+ `held`, `paused` or `off`.
73
+ - **Focus:** before a slide becomes inert, focus moves to its page button or
74
+ another control, so it is never lost.
75
+
76
+ The enhancer sets `data-ready`, `data-overflow`, `data-row`, `data-dragging`
77
+ and `data-scrubbing` for styling, and hides the controls when nothing overflows.
78
+ `enhanceCarousel(root)` returns `{ go, goTo, pause, play }`.
79
+
80
+ ### Theming
81
+
82
+ Use `carouselStyles` once per page (for example
83
+ `<style set:html={carouselStyles}>`) and set custom properties on the carousel:
84
+
85
+ `--system-carousel-gap`, `-slide` (slide width), `-inset` (full-bleed side
86
+ padding), `-align`, `-ease`, `-focus`, `-button-size`, `-button-border`,
87
+ `-button-radius`, `-button-background`, `-button-hover-background`,
88
+ `-button-hover-border`, `-controls-justify`, `-controls-gap`,
89
+ `-controls-offset`, `-scrollbar-width`, `-scrollbar-thickness`,
90
+ `-scrollbar-track`, `-scrollbar-thumb`, `-thumb-width`, `-thumb-height`,
91
+ `-thumb-current`, `-recede-scale`, `-recede-fade`, `-fade-ms`.
92
+
93
+ ## Disclosures
94
+
95
+ `enhanceDisclosures(root)` animates native `<details>`: open and close can be
96
+ reversed mid-way, `data-state` (`open` or `closed`) follows the reader's intent
97
+ at once so icons turn with the click, and reduced motion opens instantly.
98
+ Keyboard and no-script behaviour stay native.
99
+
100
+ ## Scroll lock
101
+
102
+ `lockScroll(owner)` and `unlockScroll(owner)` implement the layout-stable host
103
+ lock described in [portable presentation](portable-presentation.md). The lock
104
+ hides page scrolling, pays the hidden scrollbar back as body end padding,
105
+ exposes it as `--scroll-lock-gap` for fixed and sticky elements (for example
106
+ `right: var(--scroll-lock-gap, 0px)` on a fixed panel), and counts owners so one
107
+ closing overlay cannot release another. The gap is measured as the growth of the
108
+ body's laid-out width, which stays correct with `scrollbar-gutter: stable` and
109
+ with overlay scrollbars.
110
+
111
+ ## Motion
112
+
113
+ `animationAllowed()`, `onMotionChange(listener)` and `animate(element, frames,
114
+ options)` give one owner for motion: the reduced-motion preference and hidden
115
+ tabs stop every effect started through them. `startMotionWatch()` begins
116
+ watching; the carousel and disclosures call it as needed.
@@ -8,8 +8,8 @@ commands below run locally.
8
8
  ## Find the source and choose a release
9
9
 
10
10
  The maintained public source is [MNSTRY/atelier](https://github.com/MNSTRY/atelier),
11
- on `main`. In the 20 October 2026 source/release snapshot, its package manifest
12
- describes the `0.2.0-alpha.15` candidate. A version in source is not a publication receipt.
11
+ on `main`. Its package manifest names the version being prepared; a version in
12
+ source is not a publication receipt.
13
13
  The example below pins the published `0.2.0-alpha.14` alpha release. Registry
14
14
  `latest` and `alpha` are separate channels; check them before choosing an upgrade:
15
15
 
@@ -69,7 +69,9 @@
69
69
  "0.2.0-alpha.12",
70
70
  "0.2.0-alpha.13",
71
71
  "0.2.0-alpha.14",
72
- "0.2.0-alpha.15"
72
+ "0.2.0-alpha.15",
73
+ "0.2.0-alpha.16",
74
+ "0.2.0-alpha.17"
73
75
  ],
74
76
  "packRefs": []
75
77
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnstry/atelier",
3
- "version": "0.2.0-alpha.15",
3
+ "version": "0.2.0-alpha.17",
4
4
  "private": false,
5
5
  "description": "Local toolkit that turns a Git repository into an ontology-governed knowledge graph, enforcement layer, and runtime for people, agents, and tools. No telemetry or required hosted account; optional host-bound vault adapters.",
6
6
  "keywords": [
@@ -64,6 +64,8 @@
64
64
  "./readiness-protocols": "./src/readiness-protocols/bundled-pack.mjs",
65
65
  "./server": "./src/server/local-sidecar.mjs",
66
66
  "./ui": "./src/ui/control-system.mjs",
67
+ "./ui/primitives": "./src/ui/primitives/index.mjs",
68
+ "./ui/primitives/browser": "./src/ui/primitives/browser.mjs",
67
69
  "./harness": "./src/harness/context.mjs",
68
70
  "./collaboration": "./src/collaboration/index.mjs",
69
71
  "./support": "./src/support/support-bundle.mjs",
@@ -106,11 +106,11 @@ This project owns its custom data. Atelier generated files only; no account, dat
106
106
  process, listener, deployment or root installation was created.
107
107
 
108
108
  1. Install the SDK and run npm run typecheck. Keep the generated lockfile.
109
- The candidate SDK 0.2.0-alpha.15 is unpublished. Until its release, run
110
- npm pack --ignore-scripts in the reviewed Atelier checkout and install its
111
- resulting tarball here with npm install /absolute/path/to/mnstry-atelier-0.2.0-alpha.15.tgz.
112
- After reviewing that dependency edit, run atelier extension seal --directory .
113
- Once the pinned SDK version is published, ordinary npm install can resolve it.
109
+ The SDK is pinned to the Atelier version that generated this project. If that
110
+ version is not on npm yet (an unreleased checkout), run npm pack --ignore-scripts
111
+ in the reviewed Atelier checkout and install the resulting tarball here by its
112
+ absolute path. After reviewing that dependency edit, run
113
+ atelier extension seal --directory .
114
114
  2. Select a customer-owned PostgreSQL 17 database. Create a restricted extension_app
115
115
  login and apply migrations/001-initial.sql with a separate migration role.
116
116
  Keep its DSN in your backend secret store. Do not use the root runtime database.
@@ -1,6 +1,6 @@
1
1
  // Browser rooms for the graph workspace. Renderer packages are injected by the host.
2
2
  export { mountGraphWorkspace, applyTheme } from './workspace.mjs'
3
- export { createConstellation } from './constellation.mjs'
3
+ export { createMap, createConstellation } from './map.mjs'
4
4
  export { createWorkbench } from './workbench.mjs'
5
5
  export { renderReview } from './review.mjs'
6
6
  export { GRAPH_WORKSPACE_CSS, injectGraphWorkspaceStyles } from './styles.mjs'
@@ -2,7 +2,7 @@
2
2
  // deps = { Sigma, Graph, EdgeArrowProgram, FA2Layout? } (sigma 3, graphology,
3
3
  // sigma/rendering's EdgeArrowProgram, graphology-layout-forceatlas2/worker).
4
4
  //
5
- // The constellation: every readable record, alive. Sigma (WebGL) draws nodes and
5
+ // The map: every readable record, alive. Sigma (WebGL) draws nodes and
6
6
  // edges; two canvases around it draw zone glows (behind) and labels (in front).
7
7
  // Labels are placed by priority with collision checks, faded by zoom, and drawn
8
8
  // with a halo instead of a box. Hovering lights a neighbourhood; selecting keeps it.
@@ -14,7 +14,7 @@ const reduced = () => matchMedia('(prefers-reduced-motion: reduce)').matches
14
14
  const hexToRgba = (hex, a) => { const n = parseInt(hex.slice(1, 7), 16); return `rgba(${n >> 16},${(n >> 8) & 255},${n & 255},${a})` }
15
15
  const mix = (a, b, t) => { const p = parseInt(a.slice(1, 7), 16), q = parseInt(b.slice(1, 7), 16); const c = s => Math.round(((p >> s) & 255) * (1 - t) + ((q >> s) & 255) * t); return '#' + [16, 8, 0].map(s => c(s).toString(16).padStart(2, '0')).join('') }
16
16
 
17
- export function createConstellation({ container, model, palette, font = 'system-ui, sans-serif', deps, reserveSelector = '[data-graph-reserve]', onSelect = () => {}, onHover = () => {}, onOpen = () => {} }) {
17
+ export function createMap({ container, model, palette, font = 'system-ui, sans-serif', deps, reserveSelector = '[data-graph-reserve]', onSelect = () => {}, onHover = () => {}, onOpen = () => {} }) {
18
18
  const { Sigma, Graph, EdgeArrowProgram, FA2Layout = null } = deps ?? {}
19
19
  if (typeof Sigma !== 'function' || typeof Graph !== 'function' || typeof EdgeArrowProgram !== 'function') throw new TypeError('Inject sigma 3 (Sigma, EdgeArrowProgram) and graphology (Graph)')
20
20
  if (model.nodes.some(n => !Number.isFinite(n.x) || !Number.isFinite(n.y))) throw new TypeError('Lay the model out first (layoutWorkspace or stored positions)')
@@ -39,6 +39,8 @@ export function createConstellation({ container, model, palette, font = 'system-
39
39
  labelCanvas.setAttribute('aria-hidden', 'true')
40
40
  const sigmaHost = document.createElement('div')
41
41
  sigmaHost.className = 'layer-sigma'
42
+ // The room styles target .map; a container marked only .constellation (alpha.15) gets both.
43
+ container.classList.add('map')
42
44
  container.replaceChildren(zoneCanvas, sigmaHost, labelCanvas)
43
45
 
44
46
  // Node sizes are in screen pixels; a small viewport needs proportionally smaller dots.
@@ -367,3 +369,6 @@ export function createConstellation({ container, model, palette, font = 'system-
367
369
  }
368
370
  return api
369
371
  }
372
+
373
+ /** Deprecated alpha.15 name of `createMap`, kept for one release. */
374
+ export const createConstellation = createMap
@@ -1,6 +1,6 @@
1
1
  // Scoped styles for the default graph workspace shell (CSS nesting under .atelier-gw).
2
2
  // Colours, type and motion come from the theme's custom properties.
3
- export const GRAPH_WORKSPACE_CSS = ".atelier-gw{container:atelier-gw / inline-size;position:relative;background:var(--canvas);color:var(--ink);font-family:var(--font-sans);font-size:14px;line-height:1.45;-webkit-font-smoothing:antialiased;overflow:hidden;\n/* V6: minimal chrome around a full-bleed constellation; quiet, focused rooms. */\n*{box-sizing:border-box}\n[hidden]{display:none!important}\n\nbutton,input,select,textarea{font:inherit;color:inherit}\nbutton{cursor:pointer;background:none;border:0}\nbutton:disabled{opacity:.45;cursor:default}\n:focus-visible{outline:2px solid var(--focus);outline-offset:2px}\nh2,h3,p{margin:0}\n.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n.skip{position:absolute;z-index:100;left:12px;top:10px;transform:translateY(-200%);background:var(--accent);color:var(--accent-ink);padding:8px 12px;border-radius:var(--radius)}\n.skip:focus{transform:none}\n.eyebrow{display:flex;align-items:center;gap:7px;font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);font-weight:600;margin-bottom:6px}\n.label{font-size:11.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);font-weight:600;margin:14px 0 6px}\n.dot{display:inline-block;width:9px;height:9px;border-radius:50%;flex:none}\n.dot-concept{display:inline-block;width:9px;height:9px;transform:rotate(45deg);background:var(--accent);flex:none;margin-right:2px}\n.primary{background:var(--accent);color:var(--accent-ink);font-weight:600;padding:9px 16px;border-radius:999px;min-height:38px}\n.secondary{border:1px solid var(--line-strong);padding:7px 14px;border-radius:999px;min-height:36px}\n.secondary.small{min-height:30px;padding:3px 11px;font-size:12.5px}\n.quiet{color:var(--ink-2);padding:8px 12px;border-radius:999px;min-height:36px}\n.quiet:hover,.secondary:hover{background:var(--surface-2)}\n.link{color:var(--accent);text-decoration:underline;text-underline-offset:3px;padding:2px 0}\n\n.stage{position:relative;width:100%;height:100%;overflow:hidden}\n.constellation{position:absolute;inset:0}\n.constellation>*{position:absolute;inset:0}\n.layer-zones{z-index:0}\n.layer-sigma{z-index:1}\n.layer-labels{z-index:2;pointer-events:none}\n&.text-mode .constellation,&.text-mode .zoom{visibility:hidden}\n\n/* chrome */\n.top{position:absolute;z-index:10;top:14px;left:14px;right:14px;display:grid;grid-template-columns:1fr minmax(240px,560px) 1fr;gap:10px;align-items:start;pointer-events:none}\n.top>*{pointer-events:auto}\n.top-right{display:flex;gap:8px;justify-self:end}\n.pill{display:inline-flex;align-items:center;gap:8px;background:var(--surface-glass);border:1px solid var(--line);border-radius:999px;padding:8px 14px;min-height:38px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);font-weight:500}\n.pill.icon{width:40px;justify-content:center;padding:0;font-size:16px}\n.pill[aria-pressed=true]{background:var(--accent);color:var(--accent-ink);border-color:transparent}\n.emblem{color:var(--accent);font-size:16px}\n.caret{color:var(--ink-3);font-size:11px}\n.menu-wrap{position:relative}\n.menu{position:absolute;top:46px;left:0;z-index:30;min-width:300px;max-width:360px;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:6px}\n.top-right .menu{left:auto;right:0}\n.menu-item{display:flex;flex-direction:column;align-items:flex-start;width:100%;text-align:left;padding:9px 12px;border-radius:var(--radius)}\n.menu-item small{color:var(--ink-3);font-size:12px}\n.menu-item:hover,.menu-item[aria-checked=true]{background:var(--surface-2)}\n.menu-item[aria-checked=true] span::after{content:' ✓';color:var(--accent)}\n.menu-label{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);padding:10px 12px 4px}\n.menu-note{font-size:12px;color:var(--ink-3);padding:4px 12px}\n.display .row{display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:13.5px}\n.display .row.col{flex-direction:column;align-items:stretch;gap:4px}\n.display input[type=checkbox]{width:16px;height:16px;accent-color:var(--accent)}\n.display input[type=range]{accent-color:var(--accent)}\n.command-wrap{position:relative;justify-self:center;width:100%}\n.command-wrap input{width:100%;background:var(--surface-glass);border:1px solid var(--line);border-radius:999px;padding:9px 52px 9px 18px;min-height:40px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}\n.command-wrap input::placeholder{color:var(--ink-3)}\n.command-wrap kbd{position:absolute;right:14px;top:11px;font-size:11px;color:var(--ink-3);border:1px solid var(--line);border-radius:6px;padding:0 6px;font-family:inherit}\n.command-results{list-style:none;margin:6px 0 0;padding:6px;position:absolute;left:0;right:0;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);max-height:60vh;overflow:auto}\n.cmd{display:flex;flex-direction:column;align-items:flex-start;width:100%;text-align:left;padding:8px 12px;border-radius:var(--radius)}\n.cmd:hover,.cmd:focus-visible,[aria-selected=true]>.cmd{background:var(--surface-2)}\n.cmd-hint{font-size:12px;color:var(--ink-3)}\n.corner{position:absolute;z-index:5;left:18px;bottom:16px;display:flex;flex-direction:column;gap:2px;font-size:12px;color:var(--ink-3);max-width:min(520px,60vw);pointer-events:none}\n.corner strong{color:var(--ink-2);font-weight:600;font-size:13px}\n.provenance{color:var(--warn)}\n&[data-private=false] .provenance{color:var(--ink-3)}\n.zoom{position:absolute;z-index:5;right:16px;bottom:16px;display:flex;flex-direction:column;background:var(--surface-glass);border:1px solid var(--line);border-radius:12px;overflow:hidden;backdrop-filter:blur(14px)}\n.zoom button{width:38px;height:36px;font-size:17px;color:var(--ink-2)}\n.zoom button+button{border-top:1px solid var(--line)}\n.zoom button:hover{background:var(--surface-2)}\n.toast{position:absolute;z-index:40;left:50%;top:70px;transform:translateX(-50%);background:var(--surface);border:1px solid var(--line);padding:8px 14px;border-radius:999px;box-shadow:var(--shadow);font-size:13px}\n\n/* selection card */\n.card{position:absolute;z-index:8;left:50%;bottom:18px;transform:translateX(-50%);width:min(560px,calc(100vw - 140px));background:var(--surface-glass);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:16px 18px 14px;backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);animation:rise var(--motion) var(--ease)}\n@keyframes rise{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}\n.card h2{font-family:var(--font-display);font-size:19px;font-weight:600;letter-spacing:-.01em;padding-right:28px}\n.card-summary{color:var(--ink-2);margin-top:4px}\n.card-meta{color:var(--ink-3);font-size:12.5px;margin-top:6px}\n.card-actions{display:flex;gap:8px;margin-top:12px;align-items:center}\n.card-close{position:absolute;right:10px;top:8px;width:32px;height:32px;border-radius:50%;color:var(--ink-3);font-size:18px}\n.card-close:hover{background:var(--surface-2)}\n.card-neighbours{margin-top:10px;font-size:13px}\n.card-neighbours summary{cursor:pointer;color:var(--ink-2)}\n.card-neighbours ul{list-style:none;margin:6px 0 0;padding:0;max-height:150px;overflow:auto;columns:2;column-gap:12px}\n.link-row{display:flex;gap:8px;text-align:left;width:100%;padding:4px 6px;border-radius:8px;color:var(--ink-2)}\n.link-row:hover{background:var(--surface-2);color:var(--ink)}\n.dir{color:var(--accent);width:14px;flex:none}\n.question-card{position:absolute;z-index:8;top:66px;left:14px;width:min(360px,calc(100vw - 28px));background:var(--surface-glass);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:14px 16px;backdrop-filter:blur(18px)}\n.question-card select{width:100%;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:8px 10px;margin-bottom:8px}\n.q-lit{font-weight:600}\n.q-note{font-size:12.5px;color:var(--ink-3);margin:6px 0 10px}\n&.diving .top,&.diving .corner,&.diving .zoom,&.diving .card,&.diving .question-card{opacity:0;transition:opacity 200ms}\n\n/* text view */\n.text-view{position:absolute;inset:0;z-index:3;overflow:auto;padding:76px max(20px,calc(50vw - 520px)) 40px;background:var(--canvas);display:none}\n&.text-mode .text-view{display:block}\n.tv-title{font-family:var(--font-display);font-size:22px;margin-bottom:10px}\n.text-view input[type=search]{width:100%;max-width:420px;background:var(--surface);border:1px solid var(--line);border-radius:999px;padding:8px 14px;margin-bottom:10px}\n.text-view h3{font-size:13px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);margin:14px 0 6px}\n.tv-zone{margin:6px 0;border-top:1px solid var(--line);padding-top:6px}\n.tv-zone summary{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:600;padding:6px 0}\n.tv-list{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:2px 12px}\n.tv-item{display:flex;flex-wrap:wrap;align-items:center;gap:2px 8px;width:100%;text-align:left;padding:6px 8px;border-radius:8px}\n.tv-item small{flex-basis:100%;color:var(--ink-3);font-size:12px}\n.tv-item:hover,.tv-item[aria-pressed=true]{background:var(--surface-2)}\n\n/* workbench */\n.workbench{position:absolute;inset:0;z-index:20;display:grid;grid-template-rows:auto minmax(0,1fr);background:radial-gradient(ellipse at 35% 50%,var(--surface-2),var(--canvas) 70%);animation:fade var(--motion) var(--ease)}\n@keyframes fade{from{opacity:0}to{opacity:1}}\n.wb-head{display:flex;align-items:center;gap:14px;padding:12px 18px;border-bottom:1px solid var(--line);min-height:56px}\n.back{color:var(--ink-2);padding:6px 10px;border-radius:999px}\n.back:hover{background:var(--surface-2)}\n.trail{display:flex;align-items:center;gap:6px;flex:1;min-width:0;overflow:hidden;font-size:13px;color:var(--ink-3)}\n.crumb{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px;color:var(--ink-2)}\nbutton.crumb{text-decoration:underline;text-underline-offset:3px}\n.crumb.current{color:var(--ink);font-weight:600}\n.save-status{font-size:12.5px;color:var(--ink-3);display:flex;align-items:center;gap:6px;white-space:nowrap}\n.save-status::before{content:'';width:7px;height:7px;border-radius:50%;background:var(--ink-3)}\n.save-status[data-state=saved]::before,.save-status[data-state=restored]::before{background:var(--ok)}\n.save-status[data-state=saving]::before{background:var(--warn)}\n.wb-body{display:grid;grid-template-columns:minmax(0,1fr) 440px;grid-template-rows:minmax(0,1fr);min-height:0}\n.wb-graph{position:relative;overflow:hidden}\n.wb-edges{position:absolute;inset:0;width:100%;height:100%;overflow:visible}\n.wb-cards{position:absolute;inset:0}\n.wb-edge line{stroke:var(--line-strong);stroke-width:1.4}\n.wb-edge path{fill:var(--line-strong)}\n.wb-edge.is-declared line{stroke:var(--ink-3);stroke-width:1.6}\n.wb-edge.is-declared path{fill:var(--ink-3)}\n.wb-edge.is-link line{stroke-dasharray:2 5}\n.wb-edge.is-challenge line{stroke:var(--danger);stroke-dasharray:7 4}\n.wb-edge.is-challenge path{fill:var(--danger)}\n.wb-edge.is-declares line{stroke:var(--accent);stroke-dasharray:4 4;opacity:.8}\n.wb-edge.is-declares path{fill:var(--accent)}\n.wb-card{position:absolute;left:0;top:0;width:224px;display:flex;flex-direction:column;align-items:flex-start;gap:3px;text-align:left;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:11px 13px;box-shadow:0 4px 18px rgba(0,0,0,.18);transition:border-color var(--motion)}\n.wb-card:hover{border-color:var(--line-strong)}\n.wb-center{width:280px;border:1.5px solid var(--accent);padding:14px 16px;cursor:default}\n.wb-kind{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--ink-3);letter-spacing:.04em}\n.wb-title{font-weight:600;font-size:14.5px;line-height:1.3}\n.wb-center .wb-title{font-family:var(--font-display);font-size:18px}\n.wb-summary{font-size:12.5px;color:var(--ink-2);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}\n.wb-meta{font-size:11.5px;color:var(--ink-3);display:flex;gap:6px;align-items:center;flex-wrap:wrap}\n.tag{font-weight:600;font-size:10.5px;padding:0 6px;border-radius:999px;border:1px solid var(--line-strong)}\n.tag-cited{border-color:var(--ok);color:var(--ok)}\n.tag-proposed{border-color:var(--warn);color:var(--warn);border-style:dashed}\n.wb-pill{position:absolute;left:0;top:0;z-index:2;display:inline-flex;align-items:center;gap:4px;font-size:11.5px;padding:2px 9px;border-radius:999px;background:var(--canvas);border:1px solid var(--line);color:var(--ink-2);white-space:nowrap}\n.wb-pill-arrow{display:inline-block;color:var(--ink)}\n.wb-more{position:absolute;left:16px;bottom:12px;font-size:12px;color:var(--ink-3)}\n.wb-graph.is-list{overflow:auto}\n.wb-graph.is-list .wb-cards{position:relative;min-height:100%}\n.wb-sheet{background:var(--surface);border-left:1px solid var(--line);overflow:auto;padding:22px 26px 30px}\n.wb-sheet h2{font-family:var(--font-display);font-size:24px;font-weight:600;letter-spacing:-.015em;line-height:1.2;margin-bottom:4px}\n.wb-sheet h2:focus{outline:none}\n.sheet-count{font-size:12.5px;color:var(--ink-3)}\n.step-rail{list-style:none;margin:16px 0 18px;padding:0;display:flex;flex-wrap:wrap;gap:6px}\n.step{display:inline-flex;align-items:center;gap:6px;padding:5px 11px 5px 6px;border-radius:999px;border:1px solid var(--line);font-size:12.5px;color:var(--ink-2)}\n.step[aria-current=step]{border-color:var(--accent);color:var(--ink);background:var(--surface-2)}\n.step-n{width:20px;height:20px;border-radius:50%;display:grid;place-items:center;font-size:11px;background:var(--surface-2);color:var(--ink-2)}\n.step-answered .step-n{background:var(--ok);color:var(--canvas)}\n.step-deferred .step-n{background:transparent;border:1px dashed var(--ink-3)}\n.step-attention .step-n{background:var(--warn);color:var(--canvas)}\n.step-body h3{font-family:var(--font-display);font-size:20px;font-weight:600;line-height:1.3}\n.step-help{color:var(--ink-3);font-size:13px;margin:4px 0 10px}\n.step-body textarea{width:100%;min-height:150px;resize:vertical;background:var(--canvas);border:1px solid var(--line-strong);border-radius:12px;padding:12px 14px;font-size:16px;line-height:1.55}\n.plan-wording{margin-top:8px;font-size:13px;color:var(--ink-2)}\n.plan-wording summary{cursor:pointer;color:var(--ink-3)}\n.guide{list-style:none;margin:8px 0 0;padding:0;font-size:13px}\n.guide li{padding-left:16px;position:relative;margin-top:3px}\n.guide li::before{position:absolute;left:0;font-weight:700}\n.guide-error{color:var(--danger)}.guide-error::before{content:'!'}\n.guide-warn{color:var(--warn)}.guide-warn::before{content:'•'}\n.guide-info{color:var(--ink-3)}.guide-info::before{content:'i'}\n.defer{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--ink-2);margin-top:12px}\n.defer input{width:16px;height:16px;accent-color:var(--accent)}\n.sheet-actions{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:20px;padding-top:14px;border-top:1px solid var(--line)}\n.sheet-actions.left{justify-content:flex-start;border-top:0;padding-top:0}\n.sheet-actions select{background:var(--canvas);border:1px solid var(--line);border-radius:999px;padding:7px 10px;max-width:180px}\n.sheet-note{font-size:12px;color:var(--ink-3);margin-top:12px}\n.lede{color:var(--ink-2);font-size:15px;margin:6px 0 12px}\n.facts{display:grid;grid-template-columns:96px 1fr;gap:6px 12px;font-size:13px;margin:0 0 16px}\n.facts dt{color:var(--ink-3)}.facts dd{margin:0;word-break:break-word}\n.sheet-h{font-size:13px;margin:12px 0 6px}\n.link-list,.cited,.suggest{list-style:none;margin:0;padding:0}\n.cited li,.suggest li{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--line);font-size:13.5px}\n.suggest-title,.cited li span{flex:1;min-width:0}\n.find{width:100%;background:var(--canvas);border:1px solid var(--line);border-radius:999px;padding:7px 12px;margin-top:12px}\n\n/* review */\n.review-room{position:absolute;inset:0;z-index:30;background:var(--canvas);overflow:auto;animation:fade var(--motion) var(--ease)}\n.review{max-width:860px;margin:0 auto;padding:28px 24px 60px}\n.review-head h2{font-family:var(--font-display);font-size:30px;font-weight:600;letter-spacing:-.02em;margin:12px 0 6px}\n.review-head h2:focus{outline:none}\n.review-status{color:var(--ink-2)}\n.review-status b{color:var(--ink)}\n.review-status[data-status=ready-for-human-review] b{color:var(--ok)}\n.review-status[data-status=needs-attention] b{color:var(--warn)}\n.change{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);padding:18px 20px;margin-top:16px}\n.change h3{font-size:18px;margin-bottom:8px}\n.pair{margin-top:10px}.pair-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}\n.pair small{color:var(--ink-3)}\n.pair blockquote{margin:2px 0 0;padding-left:12px;border-left:3px solid var(--accent)}\n.pair.same small{display:block}\n.line{margin-top:6px}\n.decide{margin-top:22px;padding:16px 20px;border-radius:var(--radius-lg);border:1px solid var(--line-strong)}\n.decide pre{font-size:12px;white-space:pre-wrap;color:var(--ink-2)}\n.decide summary{cursor:pointer;color:var(--ink-3);margin-top:8px}\n\n@container atelier-gw (max-width:860px){\n .wb-body{grid-template-columns:1fr;grid-template-rows:minmax(300px,48dvh) auto;overflow:auto}\n .wb-sheet{border-left:0;border-top:1px solid var(--line)}\n .workbench{overflow:auto}\n}\n@container atelier-gw (max-width:720px){\n .top{grid-template-columns:auto 1fr;top:10px;left:10px;right:10px}\n .command-wrap{grid-column:1/-1;grid-row:2}\n .top-right{grid-column:2;grid-row:1}\n .menu{min-width:min(320px,calc(100vw - 20px))}\n .card{width:calc(100vw - 20px);bottom:10px;padding:14px}\n .card-neighbours ul{columns:1}\n .corner{bottom:auto;top:112px;left:12px;max-width:calc(100vw - 24px)}\n .corner span:not(.provenance){display:none}\n .zoom{bottom:auto;top:104px;right:10px}\n .question-card{top:150px}\n .text-view{padding:120px 14px 30px}\n .wb-head{flex-wrap:wrap;gap:8px}\n .save-status{flex-basis:100%}\n .wb-sheet{padding:18px 16px 30px}\n .pair-grid{grid-template-columns:1fr}\n}\n@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}\n.step-plan .step-n{background:transparent;border:1px solid var(--accent);color:var(--accent)}\n.top>.menu-wrap{display:flex;gap:8px;align-items:flex-start}\n.primary-action{border-color:var(--accent)}\n\n}\n"
3
+ export const GRAPH_WORKSPACE_CSS = ".atelier-gw{container:atelier-gw / inline-size;position:relative;background:var(--canvas);color:var(--ink);font-family:var(--font-sans);font-size:14px;line-height:1.45;-webkit-font-smoothing:antialiased;overflow:hidden;\n/* V6: minimal chrome around a full-bleed map; quiet, focused rooms. */\n*{box-sizing:border-box}\n[hidden]{display:none!important}\n\nbutton,input,select,textarea{font:inherit;color:inherit}\nbutton{cursor:pointer;background:none;border:0}\nbutton:disabled{opacity:.45;cursor:default}\n:focus-visible{outline:2px solid var(--focus);outline-offset:2px}\nh2,h3,p{margin:0}\n.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n.skip{position:absolute;z-index:100;left:12px;top:10px;transform:translateY(-200%);background:var(--accent);color:var(--accent-ink);padding:8px 12px;border-radius:var(--radius)}\n.skip:focus{transform:none}\n.eyebrow{display:flex;align-items:center;gap:7px;font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);font-weight:600;margin-bottom:6px}\n.label{font-size:11.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);font-weight:600;margin:14px 0 6px}\n.dot{display:inline-block;width:9px;height:9px;border-radius:50%;flex:none}\n.dot-concept{display:inline-block;width:9px;height:9px;transform:rotate(45deg);background:var(--accent);flex:none;margin-right:2px}\n.primary{background:var(--accent);color:var(--accent-ink);font-weight:600;padding:9px 16px;border-radius:999px;min-height:38px}\n.secondary{border:1px solid var(--line-strong);padding:7px 14px;border-radius:999px;min-height:36px}\n.secondary.small{min-height:30px;padding:3px 11px;font-size:12.5px}\n.quiet{color:var(--ink-2);padding:8px 12px;border-radius:999px;min-height:36px}\n.quiet:hover,.secondary:hover{background:var(--surface-2)}\n.link{color:var(--accent);text-decoration:underline;text-underline-offset:3px;padding:2px 0}\n\n.stage{position:relative;width:100%;height:100%;overflow:hidden}\n.map{position:absolute;inset:0}\n.map>*{position:absolute;inset:0}\n.layer-zones{z-index:0}\n.layer-sigma{z-index:1}\n.layer-labels{z-index:2;pointer-events:none}\n&.text-mode .map,&.text-mode .zoom{visibility:hidden}\n\n/* chrome */\n.top{position:absolute;z-index:10;top:14px;left:14px;right:14px;display:grid;grid-template-columns:1fr minmax(240px,560px) 1fr;gap:10px;align-items:start;pointer-events:none}\n.top>*{pointer-events:auto}\n.top-right{display:flex;gap:8px;justify-self:end}\n.pill{display:inline-flex;align-items:center;gap:8px;background:var(--surface-glass);border:1px solid var(--line);border-radius:999px;padding:8px 14px;min-height:38px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);font-weight:500}\n.pill.icon{width:40px;justify-content:center;padding:0;font-size:16px}\n.pill[aria-pressed=true]{background:var(--accent);color:var(--accent-ink);border-color:transparent}\n.emblem{color:var(--accent);font-size:16px}\n.caret{color:var(--ink-3);font-size:11px}\n.menu-wrap{position:relative}\n.menu{position:absolute;top:46px;left:0;z-index:30;min-width:300px;max-width:360px;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:6px}\n.top-right .menu{left:auto;right:0}\n.menu-item{display:flex;flex-direction:column;align-items:flex-start;width:100%;text-align:left;padding:9px 12px;border-radius:var(--radius)}\n.menu-item small{color:var(--ink-3);font-size:12px}\n.menu-item:hover,.menu-item[aria-checked=true]{background:var(--surface-2)}\n.menu-item[aria-checked=true] span::after{content:' ✓';color:var(--accent)}\n.menu-label{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);padding:10px 12px 4px}\n.menu-note{font-size:12px;color:var(--ink-3);padding:4px 12px}\n.display .row{display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:13.5px}\n.display .row.col{flex-direction:column;align-items:stretch;gap:4px}\n.display input[type=checkbox]{width:16px;height:16px;accent-color:var(--accent)}\n.display input[type=range]{accent-color:var(--accent)}\n.command-wrap{position:relative;justify-self:center;width:100%}\n.command-wrap input{width:100%;background:var(--surface-glass);border:1px solid var(--line);border-radius:999px;padding:9px 52px 9px 18px;min-height:40px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}\n.command-wrap input::placeholder{color:var(--ink-3)}\n.command-wrap kbd{position:absolute;right:14px;top:11px;font-size:11px;color:var(--ink-3);border:1px solid var(--line);border-radius:6px;padding:0 6px;font-family:inherit}\n.command-results{list-style:none;margin:6px 0 0;padding:6px;position:absolute;left:0;right:0;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);max-height:60vh;overflow:auto}\n.cmd{display:flex;flex-direction:column;align-items:flex-start;width:100%;text-align:left;padding:8px 12px;border-radius:var(--radius)}\n.cmd:hover,.cmd:focus-visible,[aria-selected=true]>.cmd{background:var(--surface-2)}\n.cmd-hint{font-size:12px;color:var(--ink-3)}\n.corner{position:absolute;z-index:5;left:18px;bottom:16px;display:flex;flex-direction:column;gap:2px;font-size:12px;color:var(--ink-3);max-width:min(520px,60vw);pointer-events:none}\n.corner strong{color:var(--ink-2);font-weight:600;font-size:13px}\n.provenance{color:var(--warn)}\n&[data-private=false] .provenance{color:var(--ink-3)}\n.zoom{position:absolute;z-index:5;right:16px;bottom:16px;display:flex;flex-direction:column;background:var(--surface-glass);border:1px solid var(--line);border-radius:12px;overflow:hidden;backdrop-filter:blur(14px)}\n.zoom button{width:38px;height:36px;font-size:17px;color:var(--ink-2)}\n.zoom button+button{border-top:1px solid var(--line)}\n.zoom button:hover{background:var(--surface-2)}\n.toast{position:absolute;z-index:40;left:50%;top:70px;transform:translateX(-50%);background:var(--surface);border:1px solid var(--line);padding:8px 14px;border-radius:999px;box-shadow:var(--shadow);font-size:13px}\n\n/* selection card */\n.card{position:absolute;z-index:8;left:50%;bottom:18px;transform:translateX(-50%);width:min(560px,calc(100vw - 140px));background:var(--surface-glass);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:16px 18px 14px;backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);animation:rise var(--motion) var(--ease)}\n@keyframes rise{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}\n.card h2{font-family:var(--font-display);font-size:19px;font-weight:600;letter-spacing:-.01em;padding-right:28px}\n.card-summary{color:var(--ink-2);margin-top:4px}\n.card-meta{color:var(--ink-3);font-size:12.5px;margin-top:6px}\n.card-actions{display:flex;gap:8px;margin-top:12px;align-items:center}\n.card-close{position:absolute;right:10px;top:8px;width:32px;height:32px;border-radius:50%;color:var(--ink-3);font-size:18px}\n.card-close:hover{background:var(--surface-2)}\n.card-neighbours{margin-top:10px;font-size:13px}\n.card-neighbours summary{cursor:pointer;color:var(--ink-2)}\n.card-neighbours ul{list-style:none;margin:6px 0 0;padding:0;max-height:150px;overflow:auto;columns:2;column-gap:12px}\n.link-row{display:flex;gap:8px;text-align:left;width:100%;padding:4px 6px;border-radius:8px;color:var(--ink-2)}\n.link-row:hover{background:var(--surface-2);color:var(--ink)}\n.dir{color:var(--accent);width:14px;flex:none}\n.question-card{position:absolute;z-index:8;top:66px;left:14px;width:min(360px,calc(100vw - 28px));background:var(--surface-glass);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow);padding:14px 16px;backdrop-filter:blur(18px)}\n.question-card select{width:100%;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:8px 10px;margin-bottom:8px}\n.q-lit{font-weight:600}\n.q-note{font-size:12.5px;color:var(--ink-3);margin:6px 0 10px}\n&.diving .top,&.diving .corner,&.diving .zoom,&.diving .card,&.diving .question-card{opacity:0;transition:opacity 200ms}\n\n/* text view */\n.text-view{position:absolute;inset:0;z-index:3;overflow:auto;padding:76px max(20px,calc(50vw - 520px)) 40px;background:var(--canvas);display:none}\n&.text-mode .text-view{display:block}\n.tv-title{font-family:var(--font-display);font-size:22px;margin-bottom:10px}\n.text-view input[type=search]{width:100%;max-width:420px;background:var(--surface);border:1px solid var(--line);border-radius:999px;padding:8px 14px;margin-bottom:10px}\n.text-view h3{font-size:13px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);margin:14px 0 6px}\n.tv-zone{margin:6px 0;border-top:1px solid var(--line);padding-top:6px}\n.tv-zone summary{display:flex;align-items:center;gap:8px;cursor:pointer;font-weight:600;padding:6px 0}\n.tv-list{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:2px 12px}\n.tv-item{display:flex;flex-wrap:wrap;align-items:center;gap:2px 8px;width:100%;text-align:left;padding:6px 8px;border-radius:8px}\n.tv-item small{flex-basis:100%;color:var(--ink-3);font-size:12px}\n.tv-item:hover,.tv-item[aria-pressed=true]{background:var(--surface-2)}\n\n/* workbench */\n.workbench{position:absolute;inset:0;z-index:20;display:grid;grid-template-rows:auto minmax(0,1fr);background:radial-gradient(ellipse at 35% 50%,var(--surface-2),var(--canvas) 70%);animation:fade var(--motion) var(--ease)}\n@keyframes fade{from{opacity:0}to{opacity:1}}\n.wb-head{display:flex;align-items:center;gap:14px;padding:12px 18px;border-bottom:1px solid var(--line);min-height:56px}\n.back{color:var(--ink-2);padding:6px 10px;border-radius:999px}\n.back:hover{background:var(--surface-2)}\n.trail{display:flex;align-items:center;gap:6px;flex:1;min-width:0;overflow:hidden;font-size:13px;color:var(--ink-3)}\n.crumb{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:220px;color:var(--ink-2)}\nbutton.crumb{text-decoration:underline;text-underline-offset:3px}\n.crumb.current{color:var(--ink);font-weight:600}\n.save-status{font-size:12.5px;color:var(--ink-3);display:flex;align-items:center;gap:6px;white-space:nowrap}\n.save-status::before{content:'';width:7px;height:7px;border-radius:50%;background:var(--ink-3)}\n.save-status[data-state=saved]::before,.save-status[data-state=restored]::before{background:var(--ok)}\n.save-status[data-state=saving]::before{background:var(--warn)}\n.wb-body{display:grid;grid-template-columns:minmax(0,1fr) 440px;grid-template-rows:minmax(0,1fr);min-height:0}\n.wb-graph{position:relative;overflow:hidden}\n.wb-edges{position:absolute;inset:0;width:100%;height:100%;overflow:visible}\n.wb-cards{position:absolute;inset:0}\n.wb-edge line{stroke:var(--line-strong);stroke-width:1.4}\n.wb-edge path{fill:var(--line-strong)}\n.wb-edge.is-declared line{stroke:var(--ink-3);stroke-width:1.6}\n.wb-edge.is-declared path{fill:var(--ink-3)}\n.wb-edge.is-link line{stroke-dasharray:2 5}\n.wb-edge.is-challenge line{stroke:var(--danger);stroke-dasharray:7 4}\n.wb-edge.is-challenge path{fill:var(--danger)}\n.wb-edge.is-declares line{stroke:var(--accent);stroke-dasharray:4 4;opacity:.8}\n.wb-edge.is-declares path{fill:var(--accent)}\n.wb-card{position:absolute;left:0;top:0;width:224px;display:flex;flex-direction:column;align-items:flex-start;gap:3px;text-align:left;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:11px 13px;box-shadow:0 4px 18px rgba(0,0,0,.18);transition:border-color var(--motion)}\n.wb-card:hover{border-color:var(--line-strong)}\n.wb-center{width:280px;border:1.5px solid var(--accent);padding:14px 16px;cursor:default}\n.wb-kind{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--ink-3);letter-spacing:.04em}\n.wb-title{font-weight:600;font-size:14.5px;line-height:1.3}\n.wb-center .wb-title{font-family:var(--font-display);font-size:18px}\n.wb-summary{font-size:12.5px;color:var(--ink-2);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}\n.wb-meta{font-size:11.5px;color:var(--ink-3);display:flex;gap:6px;align-items:center;flex-wrap:wrap}\n.tag{font-weight:600;font-size:10.5px;padding:0 6px;border-radius:999px;border:1px solid var(--line-strong)}\n.tag-cited{border-color:var(--ok);color:var(--ok)}\n.tag-proposed{border-color:var(--warn);color:var(--warn);border-style:dashed}\n.wb-pill{position:absolute;left:0;top:0;z-index:2;display:inline-flex;align-items:center;gap:4px;font-size:11.5px;padding:2px 9px;border-radius:999px;background:var(--canvas);border:1px solid var(--line);color:var(--ink-2);white-space:nowrap}\n.wb-pill-arrow{display:inline-block;color:var(--ink)}\n.wb-more{position:absolute;left:16px;bottom:12px;font-size:12px;color:var(--ink-3)}\n.wb-graph.is-list{overflow:auto}\n.wb-graph.is-list .wb-cards{position:relative;min-height:100%}\n.wb-sheet{background:var(--surface);border-left:1px solid var(--line);overflow:auto;padding:22px 26px 30px}\n.wb-sheet h2{font-family:var(--font-display);font-size:24px;font-weight:600;letter-spacing:-.015em;line-height:1.2;margin-bottom:4px}\n.wb-sheet h2:focus{outline:none}\n.sheet-count{font-size:12.5px;color:var(--ink-3)}\n.step-rail{list-style:none;margin:16px 0 18px;padding:0;display:flex;flex-wrap:wrap;gap:6px}\n.step{display:inline-flex;align-items:center;gap:6px;padding:5px 11px 5px 6px;border-radius:999px;border:1px solid var(--line);font-size:12.5px;color:var(--ink-2)}\n.step[aria-current=step]{border-color:var(--accent);color:var(--ink);background:var(--surface-2)}\n.step-n{width:20px;height:20px;border-radius:50%;display:grid;place-items:center;font-size:11px;background:var(--surface-2);color:var(--ink-2)}\n.step-answered .step-n{background:var(--ok);color:var(--canvas)}\n.step-deferred .step-n{background:transparent;border:1px dashed var(--ink-3)}\n.step-attention .step-n{background:var(--warn);color:var(--canvas)}\n.step-body h3{font-family:var(--font-display);font-size:20px;font-weight:600;line-height:1.3}\n.step-help{color:var(--ink-3);font-size:13px;margin:4px 0 10px}\n.step-body textarea{width:100%;min-height:150px;resize:vertical;background:var(--canvas);border:1px solid var(--line-strong);border-radius:12px;padding:12px 14px;font-size:16px;line-height:1.55}\n.plan-wording{margin-top:8px;font-size:13px;color:var(--ink-2)}\n.plan-wording summary{cursor:pointer;color:var(--ink-3)}\n.guide{list-style:none;margin:8px 0 0;padding:0;font-size:13px}\n.guide li{padding-left:16px;position:relative;margin-top:3px}\n.guide li::before{position:absolute;left:0;font-weight:700}\n.guide-error{color:var(--danger)}.guide-error::before{content:'!'}\n.guide-warn{color:var(--warn)}.guide-warn::before{content:'•'}\n.guide-info{color:var(--ink-3)}.guide-info::before{content:'i'}\n.defer{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--ink-2);margin-top:12px}\n.defer input{width:16px;height:16px;accent-color:var(--accent)}\n.sheet-actions{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:20px;padding-top:14px;border-top:1px solid var(--line)}\n.sheet-actions.left{justify-content:flex-start;border-top:0;padding-top:0}\n.sheet-actions select{background:var(--canvas);border:1px solid var(--line);border-radius:999px;padding:7px 10px;max-width:180px}\n.sheet-note{font-size:12px;color:var(--ink-3);margin-top:12px}\n.lede{color:var(--ink-2);font-size:15px;margin:6px 0 12px}\n.facts{display:grid;grid-template-columns:96px 1fr;gap:6px 12px;font-size:13px;margin:0 0 16px}\n.facts dt{color:var(--ink-3)}.facts dd{margin:0;word-break:break-word}\n.sheet-h{font-size:13px;margin:12px 0 6px}\n.link-list,.cited,.suggest{list-style:none;margin:0;padding:0}\n.cited li,.suggest li{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--line);font-size:13.5px}\n.suggest-title,.cited li span{flex:1;min-width:0}\n.find{width:100%;background:var(--canvas);border:1px solid var(--line);border-radius:999px;padding:7px 12px;margin-top:12px}\n\n/* review */\n.review-room{position:absolute;inset:0;z-index:30;background:var(--canvas);overflow:auto;animation:fade var(--motion) var(--ease)}\n.review{max-width:860px;margin:0 auto;padding:28px 24px 60px}\n.review-head h2{font-family:var(--font-display);font-size:30px;font-weight:600;letter-spacing:-.02em;margin:12px 0 6px}\n.review-head h2:focus{outline:none}\n.review-status{color:var(--ink-2)}\n.review-status b{color:var(--ink)}\n.review-status[data-status=ready-for-human-review] b{color:var(--ok)}\n.review-status[data-status=needs-attention] b{color:var(--warn)}\n.change{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius-lg);padding:18px 20px;margin-top:16px}\n.change h3{font-size:18px;margin-bottom:8px}\n.pair{margin-top:10px}.pair-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}\n.pair small{color:var(--ink-3)}\n.pair blockquote{margin:2px 0 0;padding-left:12px;border-left:3px solid var(--accent)}\n.pair.same small{display:block}\n.line{margin-top:6px}\n.decide{margin-top:22px;padding:16px 20px;border-radius:var(--radius-lg);border:1px solid var(--line-strong)}\n.decide pre{font-size:12px;white-space:pre-wrap;color:var(--ink-2)}\n.decide summary{cursor:pointer;color:var(--ink-3);margin-top:8px}\n\n@container atelier-gw (max-width:860px){\n .wb-body{grid-template-columns:1fr;grid-template-rows:minmax(300px,48dvh) auto;overflow:auto}\n .wb-sheet{border-left:0;border-top:1px solid var(--line)}\n .workbench{overflow:auto}\n}\n@container atelier-gw (max-width:720px){\n .top{grid-template-columns:auto 1fr;top:10px;left:10px;right:10px}\n .command-wrap{grid-column:1/-1;grid-row:2}\n .top-right{grid-column:2;grid-row:1}\n .menu{min-width:min(320px,calc(100vw - 20px))}\n .card{width:calc(100vw - 20px);bottom:10px;padding:14px}\n .card-neighbours ul{columns:1}\n .corner{bottom:auto;top:112px;left:12px;max-width:calc(100vw - 24px)}\n .corner span:not(.provenance){display:none}\n .zoom{bottom:auto;top:104px;right:10px}\n .question-card{top:150px}\n .text-view{padding:120px 14px 30px}\n .wb-head{flex-wrap:wrap;gap:8px}\n .save-status{flex-basis:100%}\n .wb-sheet{padding:18px 16px 30px}\n .pair-grid{grid-template-columns:1fr}\n}\n@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}\n.step-plan .step-n{background:transparent;border:1px solid var(--accent);color:var(--accent)}\n.top>.menu-wrap{display:flex;gap:8px;align-items:flex-start}\n.primary-action{border-color:var(--accent)}\n\n}\n"
4
4
 
5
5
  export function injectGraphWorkspaceStyles(doc = document) {
6
6
  if (doc.getElementById('atelier-gw-styles')) return
@@ -298,12 +298,12 @@ export function createWorkbench({ root, model, palette, store, vocab, onBack, on
298
298
  }
299
299
  sheet.append(h('h3', { class: 'sheet-h', text: `Connections · ${links.length}` }), list)
300
300
  if (links.length > 40) sheet.append(h('p', { class: 'step-help', text: `${links.length - 40} more in Text view.` }))
301
- sheet.append(h('div', { class: 'sheet-actions left' }, h('button', { class: 'quiet', type: 'button', onclick: () => onShowOnMap(n.id) }, 'Show on the constellation')))
301
+ sheet.append(h('div', { class: 'sheet-actions left' }, h('button', { class: 'quiet', type: 'button', onclick: () => onShowOnMap(n.id) }, 'Show on the map')))
302
302
  }
303
303
 
304
304
  function renderHead() {
305
305
  clear(head)
306
- head.append(h('button', { class: 'back', type: 'button', id: 'wb-back', onclick: onBack }, '← Constellation'))
306
+ head.append(h('button', { class: 'back', type: 'button', id: 'wb-back', onclick: onBack }, '← Map'))
307
307
  const crumbs = h('nav', { class: 'trail', 'aria-label': 'Walked so far' })
308
308
  trail.forEach((t, i) => {
309
309
  if (i) crumbs.append(h('span', { class: 'sep', 'aria-hidden': 'true', text: '›' }))