@graview/primitives 0.1.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 (207) hide show
  1. package/LICENSE +96 -0
  2. package/README.md +25 -0
  3. package/dist/arrange-bar.d.ts +49 -0
  4. package/dist/arrange-bar.d.ts.map +1 -0
  5. package/dist/arrange-bar.js +264 -0
  6. package/dist/arrange-bar.js.map +1 -0
  7. package/dist/chat.d.ts +46 -0
  8. package/dist/chat.d.ts.map +1 -0
  9. package/dist/chat.js +343 -0
  10. package/dist/chat.js.map +1 -0
  11. package/dist/companion.d.ts +58 -0
  12. package/dist/companion.d.ts.map +1 -0
  13. package/dist/companion.js +433 -0
  14. package/dist/companion.js.map +1 -0
  15. package/dist/connections.d.ts +23 -0
  16. package/dist/connections.d.ts.map +1 -0
  17. package/dist/connections.js +100 -0
  18. package/dist/connections.js.map +1 -0
  19. package/dist/default-views.d.ts +35 -0
  20. package/dist/default-views.d.ts.map +1 -0
  21. package/dist/default-views.js +605 -0
  22. package/dist/default-views.js.map +1 -0
  23. package/dist/editable.d.ts +71 -0
  24. package/dist/editable.d.ts.map +1 -0
  25. package/dist/editable.js +225 -0
  26. package/dist/editable.js.map +1 -0
  27. package/dist/figure.d.ts +30 -0
  28. package/dist/figure.d.ts.map +1 -0
  29. package/dist/figure.js +66 -0
  30. package/dist/figure.js.map +1 -0
  31. package/dist/find.d.ts +5 -0
  32. package/dist/find.d.ts.map +1 -0
  33. package/dist/find.js +252 -0
  34. package/dist/find.js.map +1 -0
  35. package/dist/index.d.ts +51 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +44 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/installation.d.ts +17 -0
  40. package/dist/installation.d.ts.map +1 -0
  41. package/dist/installation.js +37 -0
  42. package/dist/installation.js.map +1 -0
  43. package/dist/ladder.d.ts +2 -0
  44. package/dist/ladder.d.ts.map +1 -0
  45. package/dist/ladder.js +53 -0
  46. package/dist/ladder.js.map +1 -0
  47. package/dist/lens/arranging.d.ts +41 -0
  48. package/dist/lens/arranging.d.ts.map +1 -0
  49. package/dist/lens/arranging.js +38 -0
  50. package/dist/lens/arranging.js.map +1 -0
  51. package/dist/lens/board.d.ts +146 -0
  52. package/dist/lens/board.d.ts.map +1 -0
  53. package/dist/lens/board.js +658 -0
  54. package/dist/lens/board.js.map +1 -0
  55. package/dist/lens/calendar-dates.d.ts +19 -0
  56. package/dist/lens/calendar-dates.d.ts.map +1 -0
  57. package/dist/lens/calendar-dates.js +55 -0
  58. package/dist/lens/calendar-dates.js.map +1 -0
  59. package/dist/lens/calendar-drawing.d.ts +59 -0
  60. package/dist/lens/calendar-drawing.d.ts.map +1 -0
  61. package/dist/lens/calendar-drawing.js +229 -0
  62. package/dist/lens/calendar-drawing.js.map +1 -0
  63. package/dist/lens/calendar-options.d.ts +107 -0
  64. package/dist/lens/calendar-options.d.ts.map +1 -0
  65. package/dist/lens/calendar-options.js +19 -0
  66. package/dist/lens/calendar-options.js.map +1 -0
  67. package/dist/lens/calendar-placing.d.ts +35 -0
  68. package/dist/lens/calendar-placing.d.ts.map +1 -0
  69. package/dist/lens/calendar-placing.js +113 -0
  70. package/dist/lens/calendar-placing.js.map +1 -0
  71. package/dist/lens/calendar-spans.d.ts +48 -0
  72. package/dist/lens/calendar-spans.d.ts.map +1 -0
  73. package/dist/lens/calendar-spans.js +146 -0
  74. package/dist/lens/calendar-spans.js.map +1 -0
  75. package/dist/lens/calendar-view.d.ts +34 -0
  76. package/dist/lens/calendar-view.d.ts.map +1 -0
  77. package/dist/lens/calendar-view.js +228 -0
  78. package/dist/lens/calendar-view.js.map +1 -0
  79. package/dist/lens/calendar.d.ts +43 -0
  80. package/dist/lens/calendar.d.ts.map +1 -0
  81. package/dist/lens/calendar.js +43 -0
  82. package/dist/lens/calendar.js.map +1 -0
  83. package/dist/lens/coverage.d.ts +183 -0
  84. package/dist/lens/coverage.d.ts.map +1 -0
  85. package/dist/lens/coverage.js +732 -0
  86. package/dist/lens/coverage.js.map +1 -0
  87. package/dist/lens/horizon.d.ts +14 -0
  88. package/dist/lens/horizon.d.ts.map +1 -0
  89. package/dist/lens/horizon.js +20 -0
  90. package/dist/lens/horizon.js.map +1 -0
  91. package/dist/lens/more.d.ts +16 -0
  92. package/dist/lens/more.d.ts.map +1 -0
  93. package/dist/lens/more.js +17 -0
  94. package/dist/lens/more.js.map +1 -0
  95. package/dist/lens/plan-lens.d.ts +17 -0
  96. package/dist/lens/plan-lens.d.ts.map +1 -0
  97. package/dist/lens/plan-lens.js +24 -0
  98. package/dist/lens/plan-lens.js.map +1 -0
  99. package/dist/lens/plan-state.d.ts +122 -0
  100. package/dist/lens/plan-state.d.ts.map +1 -0
  101. package/dist/lens/plan-state.js +152 -0
  102. package/dist/lens/plan-state.js.map +1 -0
  103. package/dist/lens/plan-view.d.ts +10 -0
  104. package/dist/lens/plan-view.d.ts.map +1 -0
  105. package/dist/lens/plan-view.js +576 -0
  106. package/dist/lens/plan-view.js.map +1 -0
  107. package/dist/lens/plan.d.ts +25 -0
  108. package/dist/lens/plan.d.ts.map +1 -0
  109. package/dist/lens/plan.js +25 -0
  110. package/dist/lens/plan.js.map +1 -0
  111. package/dist/lens/reach.d.ts +47 -0
  112. package/dist/lens/reach.d.ts.map +1 -0
  113. package/dist/lens/reach.js +67 -0
  114. package/dist/lens/reach.js.map +1 -0
  115. package/dist/lens/timeline.d.ts +137 -0
  116. package/dist/lens/timeline.d.ts.map +1 -0
  117. package/dist/lens/timeline.js +426 -0
  118. package/dist/lens/timeline.js.map +1 -0
  119. package/dist/markup.d.ts +23 -0
  120. package/dist/markup.d.ts.map +1 -0
  121. package/dist/markup.js +24 -0
  122. package/dist/markup.js.map +1 -0
  123. package/dist/places.d.ts +24 -0
  124. package/dist/places.d.ts.map +1 -0
  125. package/dist/places.js +227 -0
  126. package/dist/places.js.map +1 -0
  127. package/dist/popover.d.ts +28 -0
  128. package/dist/popover.d.ts.map +1 -0
  129. package/dist/popover.js +59 -0
  130. package/dist/popover.js.map +1 -0
  131. package/dist/primitives/index.d.ts +214 -0
  132. package/dist/primitives/index.d.ts.map +1 -0
  133. package/dist/primitives/index.js +381 -0
  134. package/dist/primitives/index.js.map +1 -0
  135. package/dist/profile.d.ts +65 -0
  136. package/dist/profile.d.ts.map +1 -0
  137. package/dist/profile.js +233 -0
  138. package/dist/profile.js.map +1 -0
  139. package/dist/quick-relations.d.ts +16 -0
  140. package/dist/quick-relations.d.ts.map +1 -0
  141. package/dist/quick-relations.js +183 -0
  142. package/dist/quick-relations.js.map +1 -0
  143. package/dist/relation-key.d.ts +44 -0
  144. package/dist/relation-key.d.ts.map +1 -0
  145. package/dist/relation-key.js +196 -0
  146. package/dist/relation-key.js.map +1 -0
  147. package/dist/seat.d.ts +137 -0
  148. package/dist/seat.d.ts.map +1 -0
  149. package/dist/seat.js +162 -0
  150. package/dist/seat.js.map +1 -0
  151. package/dist/seats.d.ts +21 -0
  152. package/dist/seats.d.ts.map +1 -0
  153. package/dist/seats.js +62 -0
  154. package/dist/seats.js.map +1 -0
  155. package/dist/seeding.d.ts +150 -0
  156. package/dist/seeding.d.ts.map +1 -0
  157. package/dist/seeding.js +538 -0
  158. package/dist/seeding.js.map +1 -0
  159. package/dist/shell.d.ts +74 -0
  160. package/dist/shell.d.ts.map +1 -0
  161. package/dist/shell.js +86 -0
  162. package/dist/shell.js.map +1 -0
  163. package/dist/theme.d.ts +50 -0
  164. package/dist/theme.d.ts.map +1 -0
  165. package/dist/theme.js +1692 -0
  166. package/dist/theme.js.map +1 -0
  167. package/dist/wordmark.d.ts +15 -0
  168. package/dist/wordmark.d.ts.map +1 -0
  169. package/dist/wordmark.js +55 -0
  170. package/dist/wordmark.js.map +1 -0
  171. package/dist/workbench/activity.d.ts +75 -0
  172. package/dist/workbench/activity.d.ts.map +1 -0
  173. package/dist/workbench/activity.js +259 -0
  174. package/dist/workbench/activity.js.map +1 -0
  175. package/dist/workbench/agent-seat.d.ts +51 -0
  176. package/dist/workbench/agent-seat.d.ts.map +1 -0
  177. package/dist/workbench/agent-seat.js +112 -0
  178. package/dist/workbench/agent-seat.js.map +1 -0
  179. package/dist/workbench/answer-args.d.ts +31 -0
  180. package/dist/workbench/answer-args.d.ts.map +1 -0
  181. package/dist/workbench/answer-args.js +257 -0
  182. package/dist/workbench/answer-args.js.map +1 -0
  183. package/dist/workbench/back-out.d.ts +71 -0
  184. package/dist/workbench/back-out.d.ts.map +1 -0
  185. package/dist/workbench/back-out.js +238 -0
  186. package/dist/workbench/back-out.js.map +1 -0
  187. package/dist/workbench/following.d.ts +7 -0
  188. package/dist/workbench/following.d.ts.map +1 -0
  189. package/dist/workbench/following.js +26 -0
  190. package/dist/workbench/following.js.map +1 -0
  191. package/dist/workbench/index.d.ts +23 -0
  192. package/dist/workbench/index.d.ts.map +1 -0
  193. package/dist/workbench/index.js +23 -0
  194. package/dist/workbench/index.js.map +1 -0
  195. package/dist/workbench/inspector.d.ts +27 -0
  196. package/dist/workbench/inspector.d.ts.map +1 -0
  197. package/dist/workbench/inspector.js +745 -0
  198. package/dist/workbench/inspector.js.map +1 -0
  199. package/dist/workbench/standing.d.ts +16 -0
  200. package/dist/workbench/standing.d.ts.map +1 -0
  201. package/dist/workbench/standing.js +102 -0
  202. package/dist/workbench/standing.js.map +1 -0
  203. package/dist/workbench/trail.d.ts +35 -0
  204. package/dist/workbench/trail.d.ts.map +1 -0
  205. package/dist/workbench/trail.js +150 -0
  206. package/dist/workbench/trail.js.map +1 -0
  207. package/package.json +62 -0
package/dist/theme.js ADDED
@@ -0,0 +1,1692 @@
1
+ /**
2
+ * The visual system.
3
+ *
4
+ * Two schemes, and they are not inversions of each other. Depth is the thing
5
+ * being drawn, and depth behaves differently in the two:
6
+ *
7
+ * - **Dark** is a lit control surface. Things at depth lose luminance and
8
+ * dissolve into the ground; the accent reads as light, and separation comes
9
+ * from glow and a lit edge.
10
+ * - **Light** is daylight and paper. Things at depth lose CONTRAST and gain
11
+ * haze — real atmospheric perspective — and separation comes from soft
12
+ * cast shadow, the way objects on a desk separate. Inverting the dark
13
+ * scheme would give grey-on-grey mush, because glow does not exist in
14
+ * daylight.
15
+ *
16
+ * One rule holds across both: **secondary text is a colour, never an
17
+ * opacity.** Opacity composites against whatever is behind and fails contrast
18
+ * silently. axe-core checks this on every run of `scripts/run-a11y.mjs`.
19
+ */
20
+ /*
21
+ * The palettes moved to `@graview/core`.
22
+ *
23
+ * They are data, not components, and two things needed them where React is
24
+ * not: `graview check` verifies a palette and must not import React to do it,
25
+ * and an app declaring its own brand in its domain layer needs a neutral base
26
+ * without reaching into the UI package.
27
+ */
28
+ export { DARK, LIGHT, SCHEMES } from "@graview/core";
29
+ import { SCHEMES, kitVariables, resolveKit } from "@graview/core";
30
+ /**
31
+ * The framework's own brand, expressed as an ordinary declared one.
32
+ *
33
+ * No special case for the built-ins: `themeCss` takes a brand and this is
34
+ * simply the default value. That is the only way to know a third party's
35
+ * theme goes through the same path — if the shipped one took a shortcut, the
36
+ * shortcut is where a stranger's theme would break.
37
+ */
38
+ export const GRAVIEW_BRAND = {
39
+ name: "Graview",
40
+ typography: {
41
+ body: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
42
+ mono: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace',
43
+ },
44
+ schemes: SCHEMES,
45
+ };
46
+ const VARIABLE = {
47
+ ground: "--graview-ground",
48
+ groundDeep: "--graview-ground-deep",
49
+ wash: "--graview-wash",
50
+ panel: "--graview-panel",
51
+ panelMuted: "--graview-panel-muted",
52
+ panelWarning: "--graview-panel-warning",
53
+ edge: "--graview-edge",
54
+ edgeBright: "--graview-edge-bright",
55
+ ink: "--graview-ink",
56
+ inkMuted: "--graview-ink-muted",
57
+ inkFaint: "--graview-ink-faint",
58
+ accent: "--graview-accent",
59
+ accentDim: "--graview-accent-dim",
60
+ accentInk: "--graview-accent-ink",
61
+ warn: "--graview-warn",
62
+ glow: "--graview-glow",
63
+ bar: "--graview-bar",
64
+ float: "--graview-float",
65
+ liftLow: "--graview-lift-low",
66
+ liftHigh: "--graview-lift-high",
67
+ tintAlpha: "--graview-tint-alpha",
68
+ tintLightness: "--graview-tint-lightness",
69
+ gridAlpha: "--graview-grid-alpha",
70
+ };
71
+ export function themeVariables(tokens) {
72
+ return Object.keys(VARIABLE)
73
+ .map((key) => ` ${VARIABLE[key]}: ${tokens[key]};`)
74
+ .join("\n");
75
+ }
76
+ /**
77
+ * Everything that stops moving when motion is not wanted — as rules under
78
+ * whichever root selector is asking for them.
79
+ *
80
+ * Motion is removed; the INFORMATION is not. A steady ring in the mover's
81
+ * colour says the same thing the pulse did, and someone who cannot take the
82
+ * animation still gets to watch the system work.
83
+ */
84
+ function stillness(asking, within) {
85
+ // The asking element and the element the rules live under are the same
86
+ // thing for a whole page and two different things for an embed, where the
87
+ // reader's answer is on the document and the rules must not escape the box.
88
+ const at = asking === within ? asking : `${asking} ${within}`;
89
+ return ` ${at} [data-graview-view] { transition: none; }
90
+ ${at} [data-graview-touched] > * { animation: none; }
91
+ ${at} [data-graview-view][data-graview-wrote] > *,
92
+ ${at} [data-graview-view][data-graview-read] > * {
93
+ animation: none;
94
+ box-shadow: 0 0 0 2px var(--graview-activity);
95
+ }
96
+ ${at} [data-graview-view][data-graview-read] > * { box-shadow: 0 0 0 1px var(--graview-activity); }
97
+ ${at} [data-graview-connector][data-graview-activity] { animation: none; stroke-opacity: 1; }`;
98
+ }
99
+ export function themeCss(scheme = "dark", brand = GRAVIEW_BRAND, options = {}) {
100
+ const tokens = brand.schemes[scheme];
101
+ const root = options.scope ?? ":root";
102
+ const surface = options.scope ?? "html, body";
103
+ /*
104
+ * THE ROOT IS THE READER'S; ONLY THE BODY IS OURS TO SIZE.
105
+ *
106
+ * `font: 0.875rem` was set on `html, body` together, which means it was
107
+ * set on the ROOT — so the root's own font-size became 0.875 of the
108
+ * browser's, and every `rem` in the framework then resolved against 14px
109
+ * instead of 16. A 0.78125rem label was 10.9px, not the 12.5 it was
110
+ * written as, and a reader who had set their browser to 20px got 17.5.
111
+ * Worse, it made the root font size unusable as the one place a text-size
112
+ * setting can be honoured, because the stylesheet was already occupying
113
+ * it. The body is sized; the root is left exactly as the person has it.
114
+ */
115
+ const text = options.scope ?? "body";
116
+ /*
117
+ * WHERE THE READER'S MOTION ANSWER IS WRITTEN.
118
+ *
119
+ * On the document element, always — it is a fact about the person, not
120
+ * about one embed. A scoped stylesheet therefore asks about the document
121
+ * and applies WITHIN its own box, so an embed on somebody else's page
122
+ * still honours a Graview host's setting without restyling anything of
123
+ * the host's. Nothing is emitted at `:root { ... }` in a scoped
124
+ * stylesheet, which is the rule an embed must not break.
125
+ */
126
+ const motionRoot = ":root";
127
+ const radius = brand.shape?.radius ?? 12;
128
+ const density = brand.shape?.density ?? 1;
129
+ const body = brand.typography?.body ??
130
+ 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif';
131
+ const kit = resolveKit(brand.kit);
132
+ return `${root} {
133
+ ${themeVariables(tokens)}
134
+ ${kitVariables(kit)}
135
+ --graview-font-body: ${body};
136
+ --graview-font-display: ${brand.typography?.display ?? body};
137
+ --graview-font-mono: ${brand.typography?.mono ?? 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace'};
138
+ /* Shape, as tokens, so a component never has to know whose product it is. */
139
+ --graview-radius: ${radius}px;
140
+ --graview-radius-sm: ${Math.max(2, Math.round(radius * 0.72))}px;
141
+ --graview-pad: ${Math.round(15 * density)}px;
142
+ --graview-pad-sm: ${Math.round(10 * density)}px;
143
+ --graview-gap: ${Math.round(7 * density)}px;
144
+ color-scheme: ${scheme};
145
+ }
146
+
147
+ ${surface} {
148
+ margin: 0;
149
+ background: var(--graview-ground-deep);
150
+ color: var(--graview-ink);
151
+ }
152
+
153
+ /*
154
+ * THE READER'S OWN TEXT SIZE.
155
+ *
156
+ * Every size in the framework was once an absolute pixel count, so somebody
157
+ * who sets a larger default font in their browser — the setting WCAG 1.4.4
158
+ * is about — got a Graview that ignored them completely. 0.875rem is 14px
159
+ * at the default 16px root, so nothing moves for anyone who has not asked
160
+ * for anything, and everything moves together for anyone who has.
161
+ */
162
+ ${text} {
163
+ font: 0.875rem/1.55 var(--graview-font-body);
164
+ font-variant-numeric: tabular-nums;
165
+ -webkit-font-smoothing: antialiased;
166
+ }
167
+
168
+ /* Headings and the wordmark take the display face when a brand supplies one,
169
+ and the body face when it does not — so a brand with one font is not asked
170
+ to name it twice. */
171
+ h1, h2, h3, h4, .graview-wordmark { font-family: var(--graview-font-display); }
172
+ code, kbd, samp { font-family: var(--graview-font-mono); }
173
+
174
+ /*
175
+ * A KIND'S FIGURE fills the box it is given.
176
+ *
177
+ * The art declares its own viewBox and knows nothing about where it is
178
+ * drawn — a chip at twelve pixels, a district's heading at eighteen, a kind
179
+ * card's landmark at forty — so the SIZE is the container's and the drawing
180
+ * scales into it. currentColor on the strokes means the kind's own hue
181
+ * arrives through the cascade without the art being redrawn.
182
+ */
183
+ [data-graview-figure] > svg {
184
+ width: 100%;
185
+ height: 100%;
186
+ display: block;
187
+ overflow: visible;
188
+ }
189
+
190
+ /* How far above the stack the camera is, 0..1 — REGISTERED so it can
191
+ transition. Rising to the Graview morphs the scene instead of cutting:
192
+ the square grid dissolves into the iso lattice and the districts grow up
193
+ out of their cards, all riding this one number. */
194
+ @property --graview-altitude {
195
+ syntax: "<number>";
196
+ inherits: true;
197
+ initial-value: 0;
198
+ }
199
+
200
+ /* The ground: a slow wash, so depth has something to recede into. */
201
+ .graview-ground {
202
+ position: relative;
203
+ background: var(--graview-wash), var(--graview-ground);
204
+ --graview-altitude: 0;
205
+ transition: --graview-altitude 640ms cubic-bezier(0.33, 0, 0.2, 1);
206
+ }
207
+
208
+ /*
209
+ * The altitude control rides the SAME number the scene rides — its own copy,
210
+ * because it sits beside the ground rather than inside it — on the same
211
+ * curve. So the mark morphs exactly as long as the scene does, and where an
212
+ * engine cannot register the property, both cut together: one mechanism,
213
+ * and no way for the control and the picture to disagree about the change.
214
+ */
215
+ .graview-altitude-control {
216
+ --graview-altitude: 0;
217
+ transition:
218
+ --graview-altitude 640ms cubic-bezier(0.33, 0, 0.2, 1),
219
+ color 240ms ease,
220
+ border-color 240ms ease;
221
+ }
222
+ .graview-altitude-mark * {
223
+ transform-box: fill-box;
224
+ transform-origin: center;
225
+ }
226
+ /* The ring of three kinds tightens to one ring around one node. */
227
+ .graview-altitude-mark-ring {
228
+ transform: scaleX(calc(1 - 0.48 * var(--graview-altitude)));
229
+ opacity: calc(0.55 + 0.4 * var(--graview-altitude));
230
+ }
231
+ .graview-altitude-mark-apex {
232
+ transform: translateY(calc(2.6px * var(--graview-altitude)));
233
+ }
234
+ /* The two wings gather into the centre and give their ink to the apex. */
235
+ .graview-altitude-mark-wing {
236
+ opacity: calc(0.75 * (1 - var(--graview-altitude)));
237
+ }
238
+ .graview-altitude-mark-wing[data-side="left"] {
239
+ transform: translate(calc(4.4px * var(--graview-altitude)), calc(-0.8px * var(--graview-altitude)));
240
+ }
241
+ .graview-altitude-mark-wing[data-side="right"] {
242
+ transform: translate(calc(-4.4px * var(--graview-altitude)), calc(-0.8px * var(--graview-altitude)));
243
+ }
244
+
245
+ /* A fine measure under the scene. Faint enough to feel like calibration
246
+ rather than graph paper, and it fades out at the edges so the scene has no
247
+ hard boundary. */
248
+ .graview-ground::before {
249
+ content: "";
250
+ position: absolute;
251
+ inset: 0;
252
+ pointer-events: none;
253
+ background-image:
254
+ linear-gradient(var(--graview-edge) 1px, transparent 1px),
255
+ linear-gradient(90deg, var(--graview-edge) 1px, transparent 1px);
256
+ background-size: var(--graview-kit-grid-size, 64px) var(--graview-kit-grid-size, 64px);
257
+ opacity: calc(var(--graview-kit-grid, 1) * var(--graview-grid-alpha) * (1 - var(--graview-altitude)));
258
+ mask-image: radial-gradient(120% 90% at 50% 40%, #000 30%, transparent 78%);
259
+ }
260
+
261
+ /* Content that scrolls inside a panel, and SAYS SO.
262
+ *
263
+ * A pure-CSS scroll shadow: two cover gradients painted in the panel's own
264
+ * ground scroll with the content, two shadows stay put, and so a shadow
265
+ * only shows at an edge there is more content past. No
266
+ * JavaScript, no measurement, and nothing to go stale — which matters
267
+ * because the failure it prevents is silent. macOS hides overlay scrollbars
268
+ * until you scroll, so without this a clipped list looks like a finished
269
+ * one. */
270
+ .graview-scroll {
271
+ overflow: auto;
272
+ min-height: 0;
273
+ background:
274
+ linear-gradient(var(--graview-panel-bg, var(--graview-panel)) 30%, transparent) center top,
275
+ linear-gradient(transparent, var(--graview-panel-bg, var(--graview-panel)) 70%) center bottom,
276
+ radial-gradient(farthest-side at 50% 0, ${scheme === "light" ? "rgba(20,30,32,0.30)" : "rgba(0,0,0,0.7)"}, transparent) center top,
277
+ radial-gradient(farthest-side at 50% 100%, ${scheme === "light" ? "rgba(20,30,32,0.30)" : "rgba(0,0,0,0.7)"}, transparent) center bottom;
278
+ background-repeat: no-repeat;
279
+ background-size: 100% 24px, 100% 24px, 100% 10px, 100% 10px;
280
+ background-attachment: local, local, scroll, scroll;
281
+ }
282
+ /* "There is more of this", said by the ground: a few pixels of the panel's
283
+ * own colour over the last row, stuck to the bottom of what scrolls. */
284
+ .graview-scroll[data-graview-overflowing]::after {
285
+ content: "";
286
+ position: sticky;
287
+ bottom: 0;
288
+ flex: 0 0 14px;
289
+ margin-top: calc(-14px - var(--graview-gap, 7px));
290
+ background: linear-gradient(transparent, var(--graview-panel-bg, var(--graview-panel)));
291
+ pointer-events: none;
292
+ }
293
+
294
+ /* A card on the kinds plane.
295
+ *
296
+ * Title and count only at rest — a strip of ten cards each showing three
297
+ * truncated member names is ten unreadable things, and the members are not
298
+ * what you are asking the strip. Hovering lifts one and reveals the kind's
299
+ * own description, which the declaration has always carried and nothing has
300
+ * ever shown. */
301
+ .graview-kind-card {
302
+ height: 100%;
303
+ transition: transform 170ms cubic-bezier(0.22, 1, 0.36, 1), height 170ms ease,
304
+ box-shadow 170ms ease;
305
+ /* IN THE STACK NOTHING HANGS BELOW A CARD. The iso block rests eighteen
306
+ pixels low there, invisible, ready to rise — and an invisible box that
307
+ pokes under the bottom row still made the stage scroll by nine pixels
308
+ with nowhere to scroll. Clipped (not hidden: no scroll container, no
309
+ scrollable overflow), and let out again from altitude, where the plate
310
+ floats above the card and the block stands up out of it. */
311
+ overflow: clip;
312
+ }
313
+ [data-graview-altitude] .graview-kind-card {
314
+ overflow: visible;
315
+ }
316
+ .graview-kind-card:hover,
317
+ .graview-kind-card:focus-within {
318
+ height: auto;
319
+ transform: translateY(-4px) scale(1.05);
320
+ box-shadow: var(--graview-lift-high);
321
+ position: relative;
322
+ z-index: 3;
323
+ }
324
+ /* From altitude a district is a village on its plot, not a card: hovering
325
+ it must not raise a white panel over the buildings. The lift stays in
326
+ the stack, where the card is a card. */
327
+ [data-graview-altitude] .graview-kind-card:hover,
328
+ [data-graview-altitude] .graview-kind-card:focus-within {
329
+ transform: none;
330
+ box-shadow: none;
331
+ background: transparent;
332
+ }
333
+
334
+ /* FROM ALTITUDE the ground is an ISOMETRIC LATTICE — diamond cells at the
335
+ classic 2:1 pitch, a finer far weave above, dissolving toward the
336
+ horizon. Always present, faded by the altitude number, so rising
337
+ CROSSFADES the square grid into the lattice instead of cutting. */
338
+ /* DRAWN AS TILES, one cell each. A repeating gradient at the lattice's
339
+ angle was phased from the middle of the box and seamed at the box's own
340
+ edge when repeated with an offset, so the lines never sat where the
341
+ city's cells were: a plot stood beside the grid, not on it. Each tile is
342
+ one cell wide and half a cell tall — the diamond's bounding box — with
343
+ the tile's two diagonals drawn across it, which is the whole lattice:
344
+ corners and centres of the tiles are its vertices, and a tile corner is
345
+ pinned where the city's cell (0,0) meets the canvas. The far weave is the
346
+ same tile at half size. */
347
+ .graview-ground::after {
348
+ content: "";
349
+ position: absolute;
350
+ inset: 0;
351
+ pointer-events: none;
352
+ --graview-lattice-tile: var(--graview-lattice-cell, calc(var(--graview-kit-lattice-size, 46px) * 2.2361));
353
+ --graview-lattice-line: var(--graview-edge);
354
+ background-image:
355
+ linear-gradient(to top right, transparent calc(50% - 0.5px), var(--graview-lattice-line) calc(50% - 0.5px), var(--graview-lattice-line) calc(50% + 0.5px), transparent calc(50% + 0.5px)),
356
+ linear-gradient(to top left, transparent calc(50% - 0.5px), var(--graview-lattice-line) calc(50% - 0.5px), var(--graview-lattice-line) calc(50% + 0.5px), transparent calc(50% + 0.5px)),
357
+ linear-gradient(to top right, transparent calc(50% - 0.5px), var(--graview-lattice-line) calc(50% - 0.5px), var(--graview-lattice-line) calc(50% + 0.5px), transparent calc(50% + 0.5px)),
358
+ linear-gradient(to top left, transparent calc(50% - 0.5px), var(--graview-lattice-line) calc(50% - 0.5px), var(--graview-lattice-line) calc(50% + 0.5px), transparent calc(50% + 0.5px));
359
+ background-size:
360
+ var(--graview-lattice-tile) calc(var(--graview-lattice-tile) / 2),
361
+ var(--graview-lattice-tile) calc(var(--graview-lattice-tile) / 2),
362
+ calc(var(--graview-lattice-tile) / 2) calc(var(--graview-lattice-tile) / 4),
363
+ calc(var(--graview-lattice-tile) / 2) calc(var(--graview-lattice-tile) / 4);
364
+ /* Anchored where the city's cell (0,0) meets the canvas, so a plot placed
365
+ by the map sits ON the grid a person can see, and pans with it. */
366
+ background-position: var(--graview-lattice-x, 0px) var(--graview-lattice-y, 0px);
367
+ /* Twice the square grid's weight: fields are meant to be seen, the grid is meant to be felt. */
368
+ opacity: calc(var(--graview-kit-lattice, 1) * var(--graview-grid-alpha) * 2 * var(--graview-altitude));
369
+ mask-image: linear-gradient(to top, #000 42%, rgba(0,0,0,0.35) 70%, transparent 92%);
370
+ -webkit-mask-image: linear-gradient(to top, #000 42%, rgba(0,0,0,0.35) 70%, transparent 92%);
371
+ }
372
+
373
+ /* THE GROUND UNDER A DISTRICT: its plot, drawn. Four lattice corners in
374
+ the kind's hue, a kerb, a cast shadow toward the light — the arithmetic
375
+ the layout already did, made visible, so a district stands on land
376
+ rather than floating on a hatch. Fades in with the altitude number the
377
+ lattice fades in with; a hand-placed district's kerb is dashed, which is
378
+ the pinned mark on the ground rather than a box over the drawing. */
379
+ .graview-plots {
380
+ z-index: 0;
381
+ opacity: var(--graview-altitude);
382
+ transition: opacity 640ms cubic-bezier(0.33, 0, 0.2, 1);
383
+ }
384
+ .graview-plot-tile {
385
+ fill: hsl(var(--graview-hue, 200) 45% ${scheme === "light" ? "58%" : "48%"} / ${scheme === "light" ? "0.12" : "0.2"});
386
+ stroke: hsl(var(--graview-hue, 200) 40% ${scheme === "light" ? "48%" : "62%"} / 0.55);
387
+ stroke-width: 1;
388
+ stroke-linejoin: round;
389
+ pointer-events: auto;
390
+ cursor: pointer;
391
+ filter: drop-shadow(${scheme === "light" ? "5px 4px 6px rgba(20,30,32,0.14)" : "6px 5px 8px rgba(0,0,0,0.4)"});
392
+ transition: fill 170ms ease;
393
+ }
394
+ .graview-plot-tile:hover {
395
+ fill: hsl(var(--graview-hue, 200) 45% ${scheme === "light" ? "58%" : "48%"} / ${scheme === "light" ? "0.2" : "0.3"});
396
+ }
397
+ .graview-plot[data-graview-pinned] .graview-plot-tile {
398
+ stroke-dasharray: 4 3;
399
+ }
400
+ /* THE VILLAGE on the tile: one small iso building per member in the kind's
401
+ own faces (the same roof and walls the block had), a flagged member's roof
402
+ in the warning colour, a selected member's building lit in the accent.
403
+ Architecture, not controls: the tile under them takes the click. */
404
+ .graview-village { pointer-events: none; }
405
+ .graview-building polygon { stroke-width: 0.8; }
406
+ .graview-building[data-graview-flagged] .graview-iso-roof { fill: var(--graview-warn); stroke: var(--graview-warn); }
407
+ .graview-building[data-graview-selected] polygon { stroke: var(--graview-accent); stroke-width: 1.4; }
408
+ .graview-building[data-graview-selected] .graview-iso-roof { fill: color-mix(in oklab, var(--graview-accent) 45%, var(--graview-panel)); }
409
+ .graview-village-rest {
410
+ font-size: 0.75rem;
411
+ letter-spacing: 0.06em;
412
+ fill: var(--graview-ink-muted);
413
+ }
414
+ /* THE ROADS between plots: the lattice's own two legs from kerb to kerb, a
415
+ bed between two edges in the ground's ink. Under the tiles and the
416
+ buildings, over the fields. A road the legend is asking about comes up
417
+ in the accent. */
418
+ .graview-road-edge {
419
+ fill: none;
420
+ stroke: var(--graview-ink);
421
+ stroke-opacity: ${scheme === "light" ? "0.28" : "0.4"};
422
+ stroke-width: 7;
423
+ stroke-linejoin: round;
424
+ stroke-linecap: round;
425
+ }
426
+ .graview-road-bed {
427
+ fill: none;
428
+ stroke: var(--graview-ground);
429
+ stroke-width: 5;
430
+ stroke-linejoin: round;
431
+ stroke-linecap: round;
432
+ }
433
+ .graview-road[data-graview-lit] .graview-road-edge {
434
+ stroke: var(--graview-accent);
435
+ stroke-opacity: 0.9;
436
+ }
437
+ /* FIELDS. From altitude the ground darkens a shade toward the near edge and
438
+ fades to the page at the horizon, so the lattice is land with a distance
439
+ rather than paper with a pattern. */
440
+ .graview-ground[data-graview-altitude] {
441
+ background:
442
+ linear-gradient(to top, color-mix(in oklab, var(--graview-ground) ${scheme === "light" ? "93%" : "88%"}, var(--graview-ink)) 0%, var(--graview-ground) 78%),
443
+ var(--graview-ground);
444
+ }
445
+
446
+ /* THE KEEPER'S BLOCK IN THE PROFILE, which hides itself when it is empty.
447
+ Both controls inside it draw nothing for a seat that may not administer,
448
+ and a heading with nothing under it is worse than no heading — so the
449
+ block is drawn only when it actually holds a control. */
450
+ .graview-profile-keeping { display: grid; gap: 6px; }
451
+ .graview-profile-keeping:not(:has(button, a)) { display: none; }
452
+ .graview-profile-keeping > * { justify-self: start; }
453
+
454
+ /* THE OPEN CHEVRON APPEARS WHEN REACHED FOR. Drawn on every plate at
455
+ altitude it was noise times the number of districts; it shows on hover,
456
+ on keyboard focus, and while the district is open — and stays a real
457
+ button in between, so Tab still finds it and Enter still opens. */
458
+ [data-graview-altitude] .graview-kind-open {
459
+ opacity: 0.5;
460
+ transition: opacity 150ms ease;
461
+ }
462
+ /* Quiet: the chevron alone until reached for. Never opacity zero — a
463
+ button nobody can see is a button a driven browser cannot press either,
464
+ and Tab must land on something that looks like something. */
465
+ [data-graview-altitude] .graview-kind-open-word {
466
+ display: none;
467
+ }
468
+ [data-graview-altitude] .graview-kind-card:hover .graview-kind-open,
469
+ [data-graview-altitude] .graview-kind-card:focus-within .graview-kind-open,
470
+ [data-graview-altitude] .graview-kind-open[aria-expanded="true"] {
471
+ opacity: 1;
472
+ }
473
+ [data-graview-altitude] .graview-kind-card:hover .graview-kind-open-word,
474
+ [data-graview-altitude] .graview-kind-card:focus-within .graview-kind-open-word,
475
+ [data-graview-altitude] .graview-kind-open[aria-expanded="true"] .graview-kind-open-word {
476
+ display: inline;
477
+ }
478
+ /* The district-open control and its roster: altitude-only chrome. Inside
479
+ the stack expanding dissolves a card, so the control does not exist
480
+ there. A full fingertip even though the glyph is small — the audit holds
481
+ every control to 24px. */
482
+ .graview-kind-open { display: none; }
483
+ [data-graview-altitude] .graview-kind-open {
484
+ display: inline-flex;
485
+ align-items: center;
486
+ justify-content: center;
487
+ flex: 0 0 auto;
488
+ /* A FINGERTIP, AND THE READER'S FINGERTIP IF IT IS BIGGER. 24px is the
489
+ floor audit-ui holds every control to; 1.5rem is the same 24 at the
490
+ default text size and grows with a reader who asked for more, so the
491
+ control never drifts under the words it sits beside. A max() rather than
492
+ either one alone: rem alone drops below the fingertip at a smaller
493
+ setting, px alone ignores the setting altogether. */
494
+ min-height: max(1.5rem, 24px);
495
+ padding: 1px 9px;
496
+ margin: -3px 0;
497
+ border-radius: 999px;
498
+ border: 1px solid var(--graview-edge);
499
+ background: var(--graview-panel);
500
+ box-shadow: none;
501
+ color: var(--graview-ink-muted);
502
+ cursor: pointer;
503
+ /* IN REM, LIKE EVERY OTHER SIZE HERE. At 10px this control's words were
504
+ the one piece of text in the framework that ignored the reader's text
505
+ size completely: set to Largest, every name on the screen doubled and
506
+ "open" stayed ten pixels tall. A size a person chose and a control that
507
+ will not take it is the accessibility setting failing on its own
508
+ surface. */
509
+ font-size: 0.75rem;
510
+ letter-spacing: 0.08em;
511
+ text-transform: uppercase;
512
+ line-height: 1;
513
+ white-space: nowrap;
514
+ }
515
+ [data-graview-altitude] .graview-kind-open:hover {
516
+ color: var(--graview-accent);
517
+ border-color: var(--graview-accent-dim);
518
+ }
519
+ /* The roster reads as a LIST, one member a row — chips wrapping at their
520
+ own widths read as spilled tiles, and a district's population is a roll
521
+ call, not a mosaic. */
522
+ /* A DRIVE-IN: a dark screen standing on the plot, and the showings under
523
+ it as a marquee of real buttons. Only from altitude; the same list the
524
+ Places pills carry, drawn where the pictures live. */
525
+ .graview-drive-in {
526
+ /* Its own block under the nameplate, never a row inside the pill: the
527
+ pill is one line of name and count, and a marquee flattened into it
528
+ read as "12 • shown above The month The week". On a box the pill sits
529
+ on the roof at the top of the card, so the marquee hangs under it; on
530
+ a landmark the pill floats above the card, so the marquee takes the
531
+ card's own top edge. */
532
+ position: absolute;
533
+ left: 50%;
534
+ top: 42px;
535
+ transform: translateX(-50%);
536
+ z-index: 3;
537
+ display: grid;
538
+ justify-items: center;
539
+ gap: 4px;
540
+ animation: graview-settle 240ms ease backwards;
541
+ }
542
+ [data-graview-landmark] .graview-drive-in {
543
+ top: 4px;
544
+ }
545
+ .graview-drive-in-marquee {
546
+ display: grid;
547
+ gap: 4px;
548
+ grid-template-columns: repeat(2, 58px);
549
+ justify-content: center;
550
+ }
551
+ .graview-drive-in-marquee[data-graview-thumbs="one"] {
552
+ grid-template-columns: 120px;
553
+ }
554
+ /* THE LENSES AS PICTURES: each showing is its lens drawn small — the real
555
+ component at a fraction of its size, cut to a thumbnail — with its name
556
+ under it. The picture takes no pointer; the button around it does. */
557
+ .graview-drive-in-thumb {
558
+ position: relative;
559
+ display: grid;
560
+ gap: 2px;
561
+ justify-items: center;
562
+ padding: 2px;
563
+ min-height: max(1.75rem, 28px);
564
+ border-radius: 6px;
565
+ border: 1px solid var(--graview-edge);
566
+ background: var(--graview-panel);
567
+ color: var(--graview-ink);
568
+ font: inherit;
569
+ }
570
+ .graview-drive-in-thumb[data-graview-pressed] {
571
+ border-color: var(--graview-accent);
572
+ color: var(--graview-accent);
573
+ }
574
+ /* The press: the whole frame, laid over the picture and the title rather than
575
+ around them, so the lens drawn small never sits inside a button. */
576
+ .graview-drive-in-thumb-press {
577
+ position: absolute;
578
+ inset: -1px;
579
+ /* It fills its frame, and says the frame's floor itself: a target is sized where it is declared. */
580
+ min-height: max(1.75rem, 28px);
581
+ margin: 0;
582
+ padding: 0;
583
+ border: none;
584
+ border-radius: 6px;
585
+ background: transparent;
586
+ box-shadow: none;
587
+ cursor: pointer;
588
+ /* The card's own floor, stated: it covers the card, and the card is at least this tall. */
589
+ min-height: max(1.75rem, 28px);
590
+ }
591
+ .graview-drive-in-thumb-press:focus-visible {
592
+ outline: 2px solid var(--graview-accent);
593
+ outline-offset: 1px;
594
+ }
595
+ .graview-drive-in-thumb-picture {
596
+ display: block;
597
+ position: relative;
598
+ width: 54px;
599
+ height: 34px;
600
+ overflow: hidden;
601
+ border-radius: 3px;
602
+ background: var(--graview-panel);
603
+ pointer-events: none;
604
+ }
605
+ [data-graview-thumbs="one"] .graview-drive-in-thumb-picture {
606
+ width: 116px;
607
+ height: 70px;
608
+ }
609
+ .graview-drive-in-thumb-natural {
610
+ position: absolute;
611
+ left: 0;
612
+ top: 0;
613
+ width: 960px;
614
+ transform: scale(0.05625);
615
+ transform-origin: 0 0;
616
+ }
617
+ [data-graview-thumbs="one"] .graview-drive-in-thumb-natural {
618
+ transform: scale(0.1208);
619
+ }
620
+ .graview-drive-in-thumb-title {
621
+ font-size: 0.75rem;
622
+ line-height: 1.2;
623
+ white-space: nowrap;
624
+ /* Its own frame's width: two pictures share the marquee, and two names
625
+ each allowed the width of one picture ran into each other. */
626
+ max-width: 100%;
627
+ overflow: hidden;
628
+ text-overflow: ellipsis;
629
+ }
630
+ /* THE BILLBOARD'S FULL-SCREEN CONTROL: the one way down from a picture. */
631
+ /* THE RAIL A BILLBOARD IS MOVED BY: a title bar, in the board's own frame
632
+ ink, along its top edge. Positioned like the full-screen control beside
633
+ it — the host wraps its children, so the rail is a grandchild of the
634
+ screen box and a child selector never reaches it. Wide enough to be an
635
+ easy target, short enough that the picture underneath is still what you
636
+ see, and present only at altitude, because only up there is the board
637
+ standing on a plot it could be moved around. */
638
+ .graview-screen-grip {
639
+ display: none;
640
+ }
641
+ [data-graview-altitude] [data-graview-screen] .graview-screen-grip {
642
+ display: flex;
643
+ align-items: center;
644
+ gap: 8px;
645
+ position: absolute;
646
+ left: 0;
647
+ right: 0;
648
+ top: 0;
649
+ /* Drawn height, not CSS height: a district's plane is scaled down at
650
+ altitude, and 14 here reached the screen as an eight-pixel strip that
651
+ took three attempts to catch. */
652
+ height: 32px;
653
+ padding: 0 6px 0 12px;
654
+ box-sizing: border-box;
655
+ cursor: grab;
656
+ /* A title bar in the panel's own colours, not a grey strip: the board is
657
+ a window onto the picture, and its bar reads as the window's. */
658
+ background: var(--graview-panel-muted);
659
+ border-bottom: 1px solid var(--graview-edge);
660
+ z-index: 3;
661
+ }
662
+ [data-graview-altitude] [data-graview-screen] .graview-screen-title {
663
+ flex: 1 1 auto;
664
+ min-width: 0;
665
+ overflow: hidden;
666
+ text-overflow: ellipsis;
667
+ white-space: nowrap;
668
+ font-size: 0.8125rem;
669
+ font-weight: 600;
670
+ color: var(--graview-ink);
671
+ }
672
+ [data-graview-altitude] [data-graview-screen] .graview-screen-grip:active {
673
+ cursor: grabbing;
674
+ }
675
+ .graview-screen-fullscreen {
676
+ flex: 0 0 auto;
677
+ min-height: max(1.5rem, 24px);
678
+ padding: 1px 10px;
679
+ border-radius: 999px;
680
+ border: 1px solid var(--graview-edge);
681
+ background: var(--graview-panel);
682
+ color: var(--graview-accent);
683
+ font-size: 0.8125rem;
684
+ font-weight: 600;
685
+ white-space: nowrap;
686
+ cursor: pointer;
687
+ box-shadow: none;
688
+ }
689
+ .graview-screen-fullscreen:hover:not(:disabled) {
690
+ border-color: var(--graview-accent);
691
+ }
692
+ .graview-drive-in-marquee > button.graview-drive-in-thumb {
693
+ font-size: 0.75rem;
694
+ line-height: 1.2;
695
+ min-height: max(1.75rem, 28px);
696
+ cursor: pointer;
697
+ }
698
+ .graview-drive-in-marquee > button[aria-pressed="true"] {
699
+ border-color: var(--graview-accent);
700
+ color: var(--graview-accent);
701
+ }
702
+
703
+ /* THE OTHERS: people and their agents in the city, each a small figure in
704
+ the scene's own line vocabulary with a name under it. They move by a
705
+ transition on transform — one number, the layout tween's curve — so a
706
+ quiet city runs nothing. This tab's own seat is not drawn here: the
707
+ companion on the frame is where it speaks. */
708
+ .graview-occupants {
709
+ position: absolute;
710
+ inset: 0;
711
+ pointer-events: none;
712
+ z-index: 6;
713
+ }
714
+ .graview-figure {
715
+ position: absolute;
716
+ left: 0;
717
+ top: 0;
718
+ width: 0;
719
+ height: 0;
720
+ pointer-events: none;
721
+ transition: transform 420ms cubic-bezier(0.33, 0, 0.2, 1);
722
+ will-change: transform;
723
+ }
724
+ .graview-figure-body {
725
+ position: absolute;
726
+ left: -16px;
727
+ top: -38px;
728
+ width: 32px;
729
+ height: 38px;
730
+ min-height: max(1.5rem, 24px);
731
+ padding: 0;
732
+ border: 0;
733
+ background: transparent;
734
+ color: hsl(var(--graview-hue, 200) 50% 48%);
735
+ cursor: pointer;
736
+ pointer-events: auto;
737
+ border-radius: 6px;
738
+ }
739
+ .graview-figure-body:focus-visible {
740
+ outline: 2px solid var(--graview-accent);
741
+ outline-offset: 2px;
742
+ }
743
+ .graview-figure-body svg {
744
+ display: block;
745
+ width: 32px;
746
+ height: 38px;
747
+ overflow: visible;
748
+ }
749
+ .graview-figure[data-graview-mode="following"] .graview-figure-body {
750
+ color: var(--graview-accent);
751
+ }
752
+ .graview-figure[data-graview-mode="refused"] .graview-figure-body {
753
+ color: var(--graview-warn);
754
+ }
755
+ .graview-figure-name {
756
+ position: absolute;
757
+ left: 50%;
758
+ top: 2px;
759
+ transform: translateX(-50%);
760
+ white-space: nowrap;
761
+ font-size: 0.75rem;
762
+ letter-spacing: 0.08em;
763
+ text-transform: uppercase;
764
+ color: var(--graview-ink-muted);
765
+ pointer-events: none;
766
+ }
767
+ /* ANOTHER PERSON, on your map: a head and shoulders in their own hue, at
768
+ the plot their stop names or in the audience row of the showing they are
769
+ watching. Press to follow them; their name is under them like a robot's. */
770
+ .graview-figure[data-graview-person] .graview-figure-body {
771
+ left: -11px;
772
+ top: -24px;
773
+ width: 22px;
774
+ height: 24px;
775
+ min-height: 0;
776
+ }
777
+ .graview-figure[data-graview-person] .graview-figure-body svg {
778
+ width: 22px;
779
+ height: 24px;
780
+ }
781
+ .graview-figure[data-graview-person][data-graview-followed] .graview-figure-body {
782
+ color: var(--graview-accent);
783
+ }
784
+ /* Their robot, beside them, captioned as theirs. */
785
+ .graview-figure[data-graview-theirs] .graview-figure-body {
786
+ opacity: 0.85;
787
+ }
788
+ /* More than a row can hold, and the anonymous: a number where they stand. */
789
+ .graview-figure-count {
790
+ position: absolute;
791
+ transform: translate(-50%, -100%);
792
+ padding: 1px 7px;
793
+ border-radius: 999px;
794
+ border: 1px solid var(--graview-edge);
795
+ background: var(--graview-float);
796
+ color: var(--graview-ink-muted);
797
+ font-size: 0.75rem;
798
+ white-space: nowrap;
799
+ pointer-events: auto;
800
+ }
801
+ /* What somebody else is pointing at, outlined in their colour. */
802
+ .graview-presence-over {
803
+ position: absolute;
804
+ border: 2px solid hsl(var(--graview-hue, 200) 55% 52%);
805
+ border-radius: 8px;
806
+ pointer-events: none;
807
+ opacity: 0.75;
808
+ transition: left 200ms ease, top 200ms ease, width 200ms ease, height 200ms ease;
809
+ }
810
+ /* Off the visible ground: an indicator at the border, pointing at it. */
811
+ .graview-figure-edge {
812
+ position: absolute;
813
+ min-height: max(1.5rem, 24px);
814
+ padding: 3px 8px;
815
+ border-radius: 999px;
816
+ border: 1px solid var(--graview-edge);
817
+ background: var(--graview-float);
818
+ color: var(--graview-ink-muted);
819
+ font-size: 0.75rem;
820
+ white-space: nowrap;
821
+ pointer-events: auto;
822
+ cursor: pointer;
823
+ }
824
+
825
+ .graview-kind-members {
826
+ display: flex;
827
+ flex-direction: column;
828
+ align-items: stretch;
829
+ gap: 3px;
830
+ margin-top: 7px;
831
+ animation: graview-settle 240ms ease backwards;
832
+ }
833
+ [data-graview-altitude] .graview-kind-members > [data-graview-pick] {
834
+ width: 100%;
835
+ box-sizing: border-box;
836
+ justify-content: flex-start;
837
+ max-width: none;
838
+ }
839
+ /* An OPENED district is a PANEL, not a pill with a list stuffed in it:
840
+ name and count share the header line, the roster sits under a hairline,
841
+ and the block behind fades — the plate IS the district while it is
842
+ open, so no roof pokes out above the roster. */
843
+ [data-graview-altitude] .graview-kind-face[data-graview-opened] {
844
+ /* A header that WRAPS rather than a grid that squeezes: the name's column
845
+ was minmax(0, 1fr) beside the count's auto, so "VEHICLES" beside
846
+ "291 · +29 past · close" got 25 pixels and the count was drawn over it. */
847
+ display: flex !important;
848
+ flex-wrap: wrap;
849
+ justify-content: flex-start !important;
850
+ align-items: baseline !important;
851
+ column-gap: 10px;
852
+ row-gap: 2px;
853
+ border-radius: 12px !important;
854
+ padding: 10px 12px 11px !important;
855
+ width: 236px;
856
+ max-width: 236px;
857
+ box-shadow: var(--graview-lift-high) !important;
858
+ }
859
+ [data-graview-altitude] .graview-kind-face[data-graview-opened] .graview-kind-members {
860
+ flex: 1 0 100%;
861
+ margin-top: 8px;
862
+ padding-top: 8px;
863
+ border-top: 1px solid var(--graview-edge);
864
+ }
865
+ [data-graview-altitude] .graview-kind-block[data-graview-opened] {
866
+ opacity: calc(var(--graview-altitude) * 0.22);
867
+ }
868
+
869
+ /* WHAT KIND OF THING THIS IS, astride the focus panel's top-right edge —
870
+ the kind's dot and its name, the same thread the chips and the legend
871
+ carry. Scene chrome, so no view has to remember to say it. */
872
+ .graview-kind-tag {
873
+ position: absolute;
874
+ top: -9px;
875
+ right: 14px;
876
+ z-index: 2;
877
+ display: var(--graview-kit-tags, inline-flex);
878
+ align-items: center;
879
+ gap: 5px;
880
+ padding: 2px 8px;
881
+ border-radius: 999px;
882
+ /* The kind's own hue on the border and the words, so the tag and the
883
+ district it belongs to read as one thread. */
884
+ border: 1px solid hsl(var(--graview-hue, 200) 45% var(--graview-tint-lightness) / 0.55);
885
+ color: hsl(var(--graview-hue, 200) 45% calc(var(--graview-tint-lightness) + ${scheme === "light" ? "-32%" : "28%"}));
886
+ background: var(--graview-float);
887
+ box-shadow: var(--graview-lift-low);
888
+ font-size: 0.6875rem;
889
+ letter-spacing: 0.14em;
890
+ text-transform: uppercase;
891
+ color: var(--graview-ink-faint);
892
+ pointer-events: none;
893
+ }
894
+
895
+ /* A LINE UNDER THE POINTER says it will take the press: the invisible hit
896
+ run ghosts in, so editability is discoverable by hovering the relation
897
+ itself rather than by rumor. */
898
+ .graview-edge-hit:hover {
899
+ stroke: var(--graview-accent);
900
+ opacity: 0.3;
901
+ }
902
+
903
+ /* THE DISTRICTS THE ROW COULD NOT HOLD.
904
+ Not a district: no figure, no count, no tint of its own. A card that is
905
+ one control — how many more, and a press — and, open, a panel above the
906
+ row naming every district with what it holds. Solid rather than dashed
907
+ and faded: a dashed, translucent card read as a placeholder, and the only
908
+ way to five districts should not look like something that failed to load. */
909
+ /* A RELATION THE BAND COULD NOT HOLD: a group with its count and first names,
910
+ or the door to the rest. A card like the others, never the kind's lens small. */
911
+ .graview-band-group {
912
+ display: grid;
913
+ align-content: center;
914
+ gap: 2px;
915
+ height: 100%;
916
+ /* A card you press, held to the floor every pressable piece of chrome keeps. */
917
+ min-height: max(1.75rem, 28px);
918
+ box-sizing: border-box;
919
+ padding: 6px 10px;
920
+ border-radius: var(--graview-radius-sm, 8px);
921
+ border: 1px solid var(--graview-edge);
922
+ background: var(--graview-panel);
923
+ color: var(--graview-ink);
924
+ overflow: hidden;
925
+ cursor: pointer;
926
+ }
927
+ .graview-band-group[data-graview-band="picture"] {
928
+ border-style: dashed;
929
+ }
930
+ .graview-band-group[data-graview-emphasis="lit"] {
931
+ border-color: var(--graview-accent);
932
+ }
933
+ .graview-band-group[data-graview-emphasis="dimmed"] {
934
+ opacity: 0.5;
935
+ }
936
+ .graview-band-group-head {
937
+ display: flex;
938
+ align-items: baseline;
939
+ gap: 6px;
940
+ min-width: 0;
941
+ }
942
+ .graview-band-group-name {
943
+ flex: 1;
944
+ min-width: 0;
945
+ font-weight: 600;
946
+ font-size: 0.9375rem;
947
+ line-height: 1.25;
948
+ white-space: nowrap;
949
+ overflow: hidden;
950
+ text-overflow: ellipsis;
951
+ }
952
+ .graview-band-group-open {
953
+ font-size: 0.75rem;
954
+ color: var(--graview-accent);
955
+ }
956
+ .graview-band-group-names {
957
+ font-size: 0.8125rem;
958
+ line-height: 1.3;
959
+ color: var(--graview-ink-muted);
960
+ white-space: nowrap;
961
+ overflow: hidden;
962
+ text-overflow: ellipsis;
963
+ }
964
+ .graview-band-group-count {
965
+ color: var(--graview-ink);
966
+ font-variant-numeric: tabular-nums;
967
+ }
968
+ .graview-beyond {
969
+ position: relative;
970
+ height: 100%;
971
+ box-sizing: border-box;
972
+ border-radius: var(--graview-radius-sm, 8px);
973
+ border: 1px solid var(--graview-edge);
974
+ background: var(--graview-panel);
975
+ }
976
+ .graview-beyond-more {
977
+ display: flex;
978
+ align-items: center;
979
+ gap: 6px;
980
+ width: 100%;
981
+ height: 100%;
982
+ min-height: max(1.75rem, 28px);
983
+ padding: var(--graview-pad-sm, 8px);
984
+ box-sizing: border-box;
985
+ border: none;
986
+ border-radius: inherit;
987
+ background: none;
988
+ color: var(--graview-ink-muted);
989
+ cursor: pointer;
990
+ font: inherit;
991
+ font-size: 0.875rem;
992
+ text-align: left;
993
+ }
994
+ .graview-beyond-more:hover,
995
+ .graview-beyond[data-graview-beyond-open] .graview-beyond-more {
996
+ color: var(--graview-accent);
997
+ background: var(--graview-wash);
998
+ }
999
+ .graview-beyond-count {
1000
+ font-family: var(--graview-font-display);
1001
+ font-size: 1.1875rem;
1002
+ line-height: 1;
1003
+ color: inherit;
1004
+ }
1005
+ .graview-beyond-word {
1006
+ font-size: 0.75rem;
1007
+ letter-spacing: 0.08em;
1008
+ text-transform: uppercase;
1009
+ }
1010
+ .graview-beyond-chevron {
1011
+ margin-left: auto;
1012
+ font-size: 0.8125rem;
1013
+ }
1014
+ /* The panel stands above the card, inside the scene, and never clips: the
1015
+ card's box is a district's height, which holds a count and not a list. */
1016
+ .graview-beyond-list {
1017
+ position: absolute;
1018
+ left: 0;
1019
+ bottom: calc(100% + 6px);
1020
+ /* On the ground, over every plane — the focused card on plane zero
1021
+ painted over a panel drawn on plane two, and a menu under a card is
1022
+ no menu. Above the zoom control too, which is the only other thing
1023
+ that stands on the ground. */
1024
+ z-index: 60;
1025
+ margin: 0;
1026
+ padding: 6px;
1027
+ list-style: none;
1028
+ display: flex;
1029
+ flex-direction: column;
1030
+ gap: 1px;
1031
+ /* Its own width, not the ground's: portalled, a percentage here is the
1032
+ whole scene. */
1033
+ min-width: 200px;
1034
+ max-width: min(280px, 92%);
1035
+ max-height: 60vh;
1036
+ overflow: auto;
1037
+ border-radius: var(--graview-radius-sm, 8px);
1038
+ border: 1px solid var(--graview-edge);
1039
+ background: var(--graview-panel);
1040
+ box-shadow: 0 18px 44px -18px rgba(0, 0, 0, 0.45);
1041
+ }
1042
+ .graview-beyond-list button {
1043
+ display: flex;
1044
+ align-items: center;
1045
+ gap: 10px;
1046
+ width: 100%;
1047
+ /* A FINGERTIP WHERE IT IS ACTUALLY DRAWN, not where it was designed.
1048
+ Every other control in this sheet is floored at 24 and the audit divides
1049
+ by the plane's scale before judging it, which is right for a control
1050
+ that sits ON a large target: the chip's disclosure is small, and the
1051
+ card behind it is the size of a card. These names are not that. They are
1052
+ the ONLY way to the districts the row could not hold, and plane two is
1053
+ drawn at 0.90 — so a designed 24 meets a finger as 21.7. Designed at 28
1054
+ it is drawn at 25, which is the number the floor was always about. */
1055
+ min-height: max(1.75rem, 28px);
1056
+ padding: 0 8px;
1057
+ border: none;
1058
+ border-radius: var(--graview-radius-sm, 6px);
1059
+ background: none;
1060
+ color: var(--graview-ink);
1061
+ cursor: pointer;
1062
+ font: inherit;
1063
+ font-size: 0.875rem;
1064
+ text-align: left;
1065
+ overflow-wrap: anywhere;
1066
+ white-space: nowrap;
1067
+ }
1068
+ .graview-beyond-list button:hover {
1069
+ color: var(--graview-accent);
1070
+ background: var(--graview-wash);
1071
+ }
1072
+ .graview-beyond-list button[aria-current] {
1073
+ color: var(--graview-ink-muted);
1074
+ cursor: default;
1075
+ }
1076
+ .graview-beyond-tally {
1077
+ margin-left: auto;
1078
+ font-size: 0.75rem;
1079
+ font-variant-numeric: tabular-nums;
1080
+ color: var(--graview-ink-faint);
1081
+ }
1082
+
1083
+ /* The way into the archive, on the card that fed it: quiet, but a real
1084
+ control at a real size. */
1085
+ .graview-kind-past {
1086
+ display: inline-flex;
1087
+ align-items: center;
1088
+ flex: 0 0 auto;
1089
+ /* The same floor as the disclosure beside it — see .graview-kind-open. */
1090
+ min-height: max(1.5rem, 24px);
1091
+ padding: 1px 8px;
1092
+ margin: -3px 0;
1093
+ border-radius: 999px;
1094
+ border: 1px dashed var(--graview-edge);
1095
+ background: none;
1096
+ box-shadow: none;
1097
+ color: var(--graview-ink-faint);
1098
+ cursor: pointer;
1099
+ font-size: 0.75rem;
1100
+ white-space: nowrap;
1101
+ }
1102
+ .graview-kind-past:hover {
1103
+ color: var(--graview-accent);
1104
+ border-color: var(--graview-accent-dim);
1105
+ }
1106
+
1107
+ /* A SEARCH OVER THE CITY. The districts the words found are lit and say how
1108
+ many; the rest recede — the same dimming a selection's reach has always
1109
+ used, so a search reads as "where the thing is", not as a new mode. The
1110
+ count is the way in: a real control, the words carried down. */
1111
+ .graview-kind-card[data-graview-emphasis="dimmed"] {
1112
+ opacity: 0.42;
1113
+ transition: opacity 160ms ease;
1114
+ }
1115
+ .graview-kind-card[data-graview-emphasis="lit"] {
1116
+ z-index: 1;
1117
+ }
1118
+ .graview-kind-card[data-graview-emphasis="lit"] .graview-kind-face {
1119
+ border-color: var(--graview-accent) !important;
1120
+ box-shadow: 0 0 0 1px var(--graview-accent-dim);
1121
+ }
1122
+ .graview-kind-hits {
1123
+ display: inline-flex;
1124
+ align-items: center;
1125
+ flex: 0 0 auto;
1126
+ min-height: max(1.5rem, 24px);
1127
+ padding: 1px 8px;
1128
+ margin: -3px 0;
1129
+ border-radius: 999px;
1130
+ border: 1px solid var(--graview-accent);
1131
+ background: none;
1132
+ box-shadow: none;
1133
+ color: var(--graview-accent);
1134
+ cursor: pointer;
1135
+ font-size: 0.75rem;
1136
+ font-variant-numeric: tabular-nums;
1137
+ white-space: nowrap;
1138
+ }
1139
+ @media (prefers-reduced-motion: reduce) {
1140
+ .graview-kind-card[data-graview-emphasis="dimmed"] { transition: none; }
1141
+ }
1142
+
1143
+ /* Chrome that arrives with a state settles in rather than popping. */
1144
+ @keyframes graview-settle {
1145
+ from { opacity: 0; transform: translateY(7px); }
1146
+ }
1147
+
1148
+ /* A district STANDS: an isometric block — roof, two shaded walls — whose
1149
+ height is its population. Always in the tree, faded and settled by the
1150
+ altitude number: rising, each block grows up out of its card while the
1151
+ card flies to its ring stop — a morph, not a cut. At altitude zero it is
1152
+ fully transparent and costs nothing. */
1153
+ .graview-kind-block {
1154
+ display: block;
1155
+ /* Architecture, not a control: clicks fall through to the card, and an
1156
+ invisible in-stack block must never sit over a neighbour's tuck. */
1157
+ pointer-events: none;
1158
+ position: absolute;
1159
+ left: 8%;
1160
+ bottom: 2px;
1161
+ width: 84%;
1162
+ height: auto;
1163
+ /* Under a marquee the block keeps out of its band: the drawing scales down inside the box. */
1164
+ max-height: calc(100% - var(--graview-marquee-room, 0px));
1165
+ opacity: var(--graview-altitude);
1166
+ transform: translateY(calc((1 - var(--graview-altitude)) * 18px));
1167
+ filter: drop-shadow(${scheme === "light" ? "10px 7px 14px rgba(20,30,32,0.28)" : "12px 8px 18px rgba(0,0,0,0.6)"});
1168
+ }
1169
+ .graview-iso-roof {
1170
+ fill: hsl(var(--graview-hue, 200) ${scheme === "light" ? "48% 82%" : "42% 32%"});
1171
+ stroke: ${scheme === "light" ? "rgba(255,255,255,0.75)" : "rgba(255,255,255,0.14)"};
1172
+ stroke-width: 1;
1173
+ }
1174
+ .graview-iso-right {
1175
+ fill: hsl(var(--graview-hue, 200) ${scheme === "light" ? "40% 66%" : "40% 21%"});
1176
+ }
1177
+ .graview-iso-left {
1178
+ fill: hsl(var(--graview-hue, 200) ${scheme === "light" ? "34% 55%" : "38% 13%"});
1179
+ }
1180
+
1181
+ /* WHERE THE KIND HAS ITS OWN DRAWING, the drawing is the building.
1182
+ Same footprint as the block, same ground line, same cast shadow — the
1183
+ difference is that the district is now a person, a plot or a vehicle
1184
+ rather than one more box. Size carries the population the box's height
1185
+ used to: the rise the card computed, in a channel a drawing can use.
1186
+
1187
+ Stroke weight is set HERE rather than in the art, because the art is
1188
+ drawn to read at twenty pixels and a 1.4-unit stroke on a 24-unit box
1189
+ becomes a six-pixel marker line at a hundred. Thinned to a drafting
1190
+ line, which is what a blueprint of a thing looks like. */
1191
+ .graview-kind-landmark {
1192
+ display: block;
1193
+ left: 50%;
1194
+ /* The block's own two pixels of ground clearance, kept: at altitude zero
1195
+ every block is pushed eighteen pixels down as part of the morph, and
1196
+ without them the drawing hangs two pixels past the bottom of the scene
1197
+ and gives the whole page a scrollbar's worth of overflow. */
1198
+ bottom: 2px;
1199
+ aspect-ratio: 1;
1200
+ /* Sized by the card's HEIGHT, not its width: a district card is wider
1201
+ than it is tall, and a square drawing at 96% of the width stood a third
1202
+ of itself above the card and back through the nameplate. Population is
1203
+ in the drawing's size either way — the same rise the box's height
1204
+ carried, 10 for a district of one and 46 for the largest. */
1205
+ height: calc((100% - var(--graview-marquee-room, 0px)) * (0.72 + var(--graview-rise, 24) * 0.008));
1206
+ width: auto;
1207
+ /* AND IT KEEPS UP WITH THE WORDS. The card is laid out in pixels off the
1208
+ stage and the nameplate is sized in rem, so a reader on Largest doubled
1209
+ every name in the city while every drawing under one stayed exactly the
1210
+ size it was — a postage stamp under a headline. A floor in em ties the
1211
+ drawing to the same number the names follow; at the browser's own size
1212
+ it is below what the card already gives and nothing moves.
1213
+
1214
+ Capped at the card, because a floor alone overflowed it: the drawings
1215
+ grew past the box they stand in and were sliced off at the bottom
1216
+ edge — a plot that reads as a V rather than a bed. The card's own
1217
+ height is what the picture can actually show, and the CARD is the thing
1218
+ that still does not follow the reader's size (see below).
1219
+
1220
+ (No back ticks in here: this whole stylesheet is a template literal,
1221
+ and one of them ends it.) */
1222
+ min-height: min(5em, calc(96% - var(--graview-marquee-room, 0px)));
1223
+ /* The morph is a GROWTH from the ground line, not a drop: a block slides
1224
+ eighteen pixels down at altitude zero and grows up out of the card, and
1225
+ a drawing given the same treatment hangs below the card it belongs to —
1226
+ two pixels past the bottom of the scene on the lowest row, which is a
1227
+ scrollbar on every ground-level screen in the app. Scaling from the
1228
+ drawing's own feet is the same reading and costs nothing below. */
1229
+ transform-origin: bottom center;
1230
+ transform: translateX(-50%) scale(calc(0.55 + var(--graview-altitude) * 0.45));
1231
+ /* A cast shadow belongs to a solid; on a line drawing it is a blurred
1232
+ second copy of every stroke. The drawing keeps a hint of one so it
1233
+ still stands on the lattice rather than floating over it. */
1234
+ filter: drop-shadow(${scheme === "light" ? "6px 5px 7px rgba(20,30,32,0.16)" : "7px 6px 9px rgba(0,0,0,0.45)"});
1235
+ }
1236
+ /* The nameplate floats ABOVE a drawing rather than standing on it. On a box
1237
+ the pill sits on the roof, which is what a label does over a building; on
1238
+ a figure the same 2% put it across the head. */
1239
+
1240
+ .graview-kind-landmark [data-graview-figure] {
1241
+ width: 100% !important;
1242
+ height: 100% !important;
1243
+ color: hsl(var(--graview-hue, 200) ${scheme === "light" ? "42% 44%" : "45% 62%"}) !important;
1244
+ }
1245
+ .graview-kind-landmark svg {
1246
+ width: 100%;
1247
+ height: 100%;
1248
+ stroke-width: 0.85;
1249
+ }
1250
+
1251
+ /* From altitude the card's face becomes the district's NAMEPLATE: an
1252
+ upright pill standing on the roof, the way a label floats over a building
1253
+ in any city view — the block carries the architecture, the pill carries
1254
+ the words. Inline styles drew the in-stack card, so the pill overrides
1255
+ must outrank them. */
1256
+ .graview-kind-face {
1257
+ transition: border-radius 640ms cubic-bezier(0.33, 0, 0.2, 1),
1258
+ padding 640ms cubic-bezier(0.33, 0, 0.2, 1),
1259
+ box-shadow 640ms ease;
1260
+ }
1261
+ /* A SIGNPOST at the plot's front corner. From altitude the nameplate stood
1262
+ at the top of the card — over the back row of the village — and the
1263
+ screen had to clear it. It is planted at the front vertex of the tile
1264
+ now, on a short post, where a sign stands at the entrance to a place;
1265
+ the scene says where that vertex is in --graview-front-y. */
1266
+ [data-graview-altitude] .graview-kind-face {
1267
+ position: absolute !important;
1268
+ left: 50% !important;
1269
+ top: 2% !important;
1270
+ transform: translateX(-50%);
1271
+ width: max-content;
1272
+ /* Wider than the block when the name is: a label overflows its building
1273
+ the way a map label does. "REQUIRE / MENTS" on two lines does not. */
1274
+ max-width: none;
1275
+ white-space: nowrap;
1276
+ height: auto !important;
1277
+ flex-direction: row !important;
1278
+ align-items: baseline !important;
1279
+ gap: 7px !important;
1280
+ padding: 3px 11px !important;
1281
+ border-radius: 999px !important;
1282
+ box-shadow: var(--graview-lift-low) !important;
1283
+ z-index: 2;
1284
+ }
1285
+ [data-graview-altitude] [data-graview-plot] .graview-kind-face {
1286
+ top: calc(var(--graview-front-y) - 22px) !important;
1287
+ }
1288
+ [data-graview-altitude] [data-graview-plot] .graview-kind-face::after {
1289
+ content: "";
1290
+ position: absolute;
1291
+ left: 50%;
1292
+ top: 100%;
1293
+ width: 1px;
1294
+ height: 10px;
1295
+ background: var(--graview-ink-faint);
1296
+ }
1297
+ /* A landmark WITHOUT a plot — a nested card — keeps its plate floating
1298
+ above the drawing rather than across its head; one on a plot stands at
1299
+ the signpost like every other district (the rule above). */
1300
+ [data-graview-altitude] [data-graview-landmark]:not([data-graview-plot] *) .graview-kind-face {
1301
+ top: 0 !important;
1302
+ transform: translate(-50%, calc(-100% - 7px));
1303
+ }
1304
+ /* The drive-in's board hangs UNDER the signpost, in the ground the layout
1305
+ reserved for it below the tile's front corner, not over the village. */
1306
+ [data-graview-altitude] .graview-drive-in {
1307
+ top: calc(var(--graview-front-y, 42px) + 12px);
1308
+ }
1309
+ /* An OPENED district's listing takes the ground under the signpost; the
1310
+ board steps up over the village, which the listing covers anyway. */
1311
+ [data-graview-altitude] .graview-kind-face[data-graview-opened] ~ .graview-drive-in {
1312
+ top: calc(var(--graview-front-y, 42px) - 22px - var(--graview-marquee-room, 0px) + 8px);
1313
+ }
1314
+ /* THE LANDMARK STANDS IN THE SQUARE: its feet at the plot's centre, among
1315
+ the buildings, rather than at the card's bottom edge — which, once the
1316
+ card grew for a board, was out in the road in front of the village. */
1317
+ [data-graview-altitude] [data-graview-plot] .graview-kind-landmark {
1318
+ bottom: auto;
1319
+ top: calc(var(--graview-centre-y, 50%) + 10px);
1320
+ transform: translate(-50%, -100%) scale(calc(0.55 + var(--graview-altitude) * 0.45));
1321
+ }
1322
+ /* ZOOM, in the ground's corner: the way a map carries its own. Two
1323
+ fingertip-sized buttons and the level between them, shown from altitude. */
1324
+ .graview-zoom {
1325
+ position: absolute;
1326
+ right: 16px;
1327
+ bottom: 16px;
1328
+ /* Above the occupants: a robot walking past a control must not cover it. */
1329
+ z-index: 8;
1330
+ display: inline-flex;
1331
+ align-items: center;
1332
+ gap: 2px;
1333
+ padding: 2px;
1334
+ border-radius: 999px;
1335
+ border: 1px solid var(--graview-edge);
1336
+ background: var(--graview-panel);
1337
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
1338
+ }
1339
+ .graview-zoom-button {
1340
+ min-width: 32px;
1341
+ min-height: 32px;
1342
+ border: 0;
1343
+ border-radius: 999px;
1344
+ background: transparent;
1345
+ color: var(--graview-ink);
1346
+ font: inherit;
1347
+ font-size: 1.0625rem;
1348
+ line-height: 1;
1349
+ cursor: pointer;
1350
+ }
1351
+ .graview-zoom-button:hover:not(:disabled) {
1352
+ background: var(--graview-edge);
1353
+ }
1354
+ .graview-zoom-button:disabled {
1355
+ opacity: 0.35;
1356
+ cursor: default;
1357
+ }
1358
+ .graview-zoom-level {
1359
+ min-width: 3.2em;
1360
+ text-align: center;
1361
+ font-size: 0.8125rem;
1362
+ color: var(--graview-ink-faint);
1363
+ font-variant-numeric: tabular-nums;
1364
+ }
1365
+
1366
+ /* THE SCREEN IS A BILLBOARD at the back of the village: a frame, and two
1367
+ posts into the ground at its foot. */
1368
+ /* On the NATURAL BOX, which is the picture now: the layout cuts the
1369
+ billboard to what the lens drew (measured by the scene), never shorter
1370
+ than a screen's worth — so the box is the picture, and an empty lens is a
1371
+ header over an empty screen rather than a strip floating over the
1372
+ village. The frame used to sit on the drawing itself, from the days the
1373
+ box was as tall as the window with the picture in its top third. It
1374
+ carries the panel's own ground so the empty part of a screen is a screen
1375
+ and not the field showing through a frame. */
1376
+ [data-graview-altitude] [data-graview-screen] > [data-graview-natural],
1377
+ [data-graview-altitude] [data-graview-screen] > :not([data-graview-natural]):not(.graview-kind-tag) {
1378
+ outline: 2px solid color-mix(in oklab, var(--graview-ink) 60%, var(--graview-panel));
1379
+ outline-offset: 0;
1380
+ background: var(--graview-panel);
1381
+ }
1382
+ [data-graview-altitude] [data-graview-screen] > :not([data-graview-natural]):not(.graview-kind-tag) {
1383
+ position: relative;
1384
+ }
1385
+ [data-graview-altitude] [data-graview-screen] > [data-graview-natural]::before,
1386
+ [data-graview-altitude] [data-graview-screen] > [data-graview-natural]::after,
1387
+ [data-graview-altitude] [data-graview-screen] > :not([data-graview-natural]):not(.graview-kind-tag)::before,
1388
+ [data-graview-altitude] [data-graview-screen] > :not([data-graview-natural]):not(.graview-kind-tag)::after {
1389
+ content: "";
1390
+ position: absolute;
1391
+ top: 100%;
1392
+ width: 3px;
1393
+ height: 16px;
1394
+ background: color-mix(in oklab, var(--graview-ink) 60%, var(--graview-panel));
1395
+ }
1396
+ [data-graview-altitude] [data-graview-screen] > [data-graview-natural]::before,
1397
+ [data-graview-altitude] [data-graview-screen] > :not([data-graview-natural]):not(.graview-kind-tag)::before { left: 22%; }
1398
+ [data-graview-altitude] [data-graview-screen] > [data-graview-natural]::after,
1399
+ [data-graview-altitude] [data-graview-screen] > :not([data-graview-natural]):not(.graview-kind-tag)::after { right: 22%; }
1400
+
1401
+ /* A thing inside a view that is itself a thing: an event in a calendar, a
1402
+ person in a list. It has to look reachable, and it has to SHOW focus —
1403
+ these are the primary way anyone moves through the graph, so a keyboard
1404
+ user who cannot see where they are is stuck. */
1405
+ /* A card someone dragged into place. Marked, not decorated: a dotted tie to
1406
+ say this position is held rather than computed. */
1407
+ /* On the CHILD, not the host: the host is the layout's box, and a view
1408
+ that sizes to its content (a zoomed record, a fit panel) fills only part
1409
+ of it — a dashed box around the empty remainder read as a drawing
1410
+ mistake, not a mark. */
1411
+ /* And never on the natural box either — a view drawn scaled sits inside a
1412
+ box the size of the whole scene, and the mark belongs on the drawing, not
1413
+ on the box; nor on the kind tag, which is a label and not the thing. */
1414
+ [data-graview-pinned] > :not([data-graview-natural]):not(.graview-kind-tag),
1415
+ [data-graview-pinned] > [data-graview-natural] > * {
1416
+ outline: 1px dashed var(--graview-edge-bright);
1417
+ outline-offset: 3px;
1418
+ border-radius: 12px;
1419
+ }
1420
+ /* AND NOT FROM ALTITUDE, where a district is a village on a plot and its
1421
+ card is a box with nothing drawn in it: the dashed outline was the only
1422
+ visible part, so a hand-placed district read as an empty rounded
1423
+ rectangle sitting on the ground — several of them, in a picture that had
1424
+ no rectangles in it. The plot's own kerb goes dashed up here, which is
1425
+ the same fact said where the district actually is. */
1426
+ [data-graview-altitude] [data-graview-pinned] > :not([data-graview-natural]):not(.graview-kind-tag),
1427
+ [data-graview-altitude] [data-graview-pinned] > [data-graview-natural] > * {
1428
+ outline: none;
1429
+ }
1430
+
1431
+ [data-graview-pick] {
1432
+ cursor: pointer;
1433
+ transition: filter 140ms ease, box-shadow 140ms ease;
1434
+ }
1435
+ [data-graview-pick]:hover {
1436
+ filter: brightness(${scheme === "light" ? 0.96 : 1.18});
1437
+ }
1438
+ [data-graview-pick]:focus-visible {
1439
+ outline: 2px solid var(--graview-accent);
1440
+ outline-offset: 2px;
1441
+ border-radius: 7px;
1442
+ }
1443
+ /* On a board the keyboard ring hugs the DISC, not the disc-plus-label
1444
+ group: a rectangle drawn around a circle read as a mystery box on the
1445
+ pitch ("why does Left Midfield have this box?"), when all it ever said
1446
+ was "the keyboard is here". */
1447
+ [data-graview-slot]:focus-visible {
1448
+ outline: none;
1449
+ }
1450
+ [data-graview-slot]:focus-visible > span:first-of-type {
1451
+ box-shadow: 0 0 0 2px var(--graview-ground), 0 0 0 4px var(--graview-accent);
1452
+ }
1453
+
1454
+ /* A view is a thing you can act on. It should look like one. */
1455
+ [data-graview-view] { cursor: pointer; }
1456
+ [data-graview-view]:hover { filter: brightness(${scheme === "light" ? 0.985 : 1.12}); }
1457
+ [data-graview-view]:focus-visible {
1458
+ outline: 2px solid var(--graview-accent);
1459
+ outline-offset: 3px;
1460
+ border-radius: 12px;
1461
+ }
1462
+
1463
+ button {
1464
+ font: inherit;
1465
+ color: var(--graview-ink);
1466
+ padding: 7px 13px;
1467
+ border: 1px solid var(--graview-edge);
1468
+ border-radius: var(--graview-radius-sm, 8px);
1469
+ background: var(--graview-panel);
1470
+ box-shadow: ${scheme === "light" ? "0 1px 2px rgba(20,30,32,0.05)" : "none"};
1471
+ cursor: pointer;
1472
+ transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
1473
+ }
1474
+ button:hover:not(:disabled) {
1475
+ border-color: var(--graview-edge-bright);
1476
+ box-shadow: ${scheme === "light"
1477
+ ? "0 1px 2px rgba(20,30,32,0.06), 0 6px 16px -8px rgba(20,30,32,0.22)"
1478
+ : "0 0 0 1px var(--graview-accent-dim), 0 0 18px -6px var(--graview-accent)"};
1479
+ }
1480
+ button:focus-visible {
1481
+ outline: 2px solid var(--graview-accent);
1482
+ outline-offset: 2px;
1483
+ }
1484
+ button:disabled { opacity: 0.45; cursor: default; }
1485
+
1486
+ code { color: var(--graview-ink-muted); font-size: 0.8125rem; letter-spacing: 0.02em; }
1487
+
1488
+ /* A view host, once the scene has placed it. The transition is on filter
1489
+ only — never on layout properties, which would make every navigation
1490
+ reflow the page. */
1491
+ [data-graview-view] {
1492
+ transition: filter 260ms cubic-bezier(0.22, 1, 0.36, 1);
1493
+ }
1494
+
1495
+ /* WHAT YOU CAN PRESS IS WHAT YOU CAN SEE.
1496
+
1497
+ A host is the box the LAYOUT gave a view — a band on the ground, the whole
1498
+ scene scaled small from altitude — and a view that sizes to its content
1499
+ fills only part of it. The rest is invisible, and an invisible box must
1500
+ not be a target: a record read from altitude and pinned beside a district
1501
+ covered that district's open button with nothing at all, and the button
1502
+ stopped answering. So the host itself is out of hit-testing on the DOM
1503
+ path and only the drawn content is in; every handler still hears the
1504
+ content's events on the way up. The GPU path keeps its own rule: there
1505
+ the whole subtree stays out, because the platform's hit-test descending
1506
+ into a captured view brings the renderer down. */
1507
+ [data-graview-stage="dom"] [data-graview-view],
1508
+ [data-graview-stage="dom"] [data-graview-view] > [data-graview-natural] {
1509
+ pointer-events: none;
1510
+ }
1511
+ [data-graview-stage="dom"] [data-graview-view] > :not([data-graview-natural]),
1512
+ [data-graview-stage="dom"] [data-graview-view] > [data-graview-natural] > * {
1513
+ pointer-events: auto;
1514
+ }
1515
+ [data-graview-view][data-graview-selected] {
1516
+ filter: drop-shadow(0 0 14px var(--graview-accent-dim));
1517
+ }
1518
+
1519
+ /* A node implicated by the last change, whoever made it. The pulse is the
1520
+ same for a human edit and an agent edit, because the diff is. */
1521
+ /* THE SEAT'S MARK on what it just wrote: its own glyph, in its own hue, at
1522
+ the thing's top-right corner. It arrives with the change and fades with
1523
+ it; the companion's log is what remembers. */
1524
+ .graview-seat-marks {
1525
+ position: absolute;
1526
+ inset: 0;
1527
+ pointer-events: none;
1528
+ z-index: 7;
1529
+ }
1530
+ .graview-seat-mark {
1531
+ position: absolute;
1532
+ display: grid;
1533
+ place-items: center;
1534
+ width: 16px;
1535
+ height: 16px;
1536
+ margin: -8px 0 0 -8px;
1537
+ border-radius: 999px;
1538
+ border: 1px solid var(--graview-edge);
1539
+ background: var(--graview-float);
1540
+ color: oklch(0.62 0.16 var(--graview-hue, 250));
1541
+ font-size: 0.6875rem;
1542
+ line-height: 1;
1543
+ box-shadow: var(--graview-lift-low);
1544
+ animation: graview-seat-mark 4000ms ease-out forwards;
1545
+ }
1546
+ /* A QUESTION THE SEAT ASKED, standing at the node it is about: it says
1547
+ itself and waits, because a dot nobody can read is a question nobody
1548
+ answers. It does not fade — an unanswered question is still open. */
1549
+ .graview-seat-asking {
1550
+ position: absolute;
1551
+ transform: translate(-50%, -100%);
1552
+ display: inline-flex;
1553
+ align-items: center;
1554
+ gap: 6px;
1555
+ max-width: 260px;
1556
+ padding: 3px 9px 3px 6px;
1557
+ border-radius: 999px;
1558
+ border: 1px solid var(--graview-accent);
1559
+ background: var(--graview-float);
1560
+ color: var(--graview-ink);
1561
+ font-size: 0.75rem;
1562
+ line-height: 1.3;
1563
+ box-shadow: var(--graview-lift-low);
1564
+ pointer-events: auto;
1565
+ }
1566
+ .graview-seat-asking > [aria-hidden] {
1567
+ display: grid;
1568
+ place-items: center;
1569
+ width: 14px;
1570
+ height: 14px;
1571
+ border-radius: 999px;
1572
+ background: var(--graview-accent);
1573
+ color: var(--graview-panel);
1574
+ font-size: 0.6875rem;
1575
+ }
1576
+ .graview-seat-asking-said {
1577
+ overflow: hidden;
1578
+ text-overflow: ellipsis;
1579
+ white-space: nowrap;
1580
+ }
1581
+
1582
+ @keyframes graview-seat-mark {
1583
+ 0% { opacity: 0; transform: translateY(3px) scale(0.8); }
1584
+ 8% { opacity: 1; transform: none; }
1585
+ 80% { opacity: 1; }
1586
+ 100% { opacity: 0; }
1587
+ }
1588
+
1589
+ @keyframes graview-touched {
1590
+ 0% { box-shadow: 0 0 0 0 var(--graview-accent-dim); }
1591
+ 35% { box-shadow: 0 0 26px 3px var(--graview-accent-dim); }
1592
+ 100% { box-shadow: 0 0 0 0 transparent; }
1593
+ }
1594
+ [data-graview-touched] > * {
1595
+ animation: graview-touched 1100ms cubic-bezier(0.22, 1, 0.36, 1);
1596
+ border-radius: 10px;
1597
+ }
1598
+
1599
+ /* ------------------------------------------------------------ watching
1600
+ *
1601
+ * From outside the plane stack, activity has somewhere to HAPPEN: a kind
1602
+ * lights where an edit landed, an edge pulses where a relation was made or
1603
+ * broken, and a flag appears where a rule started failing. All of it is
1604
+ * derived from the op log — author, intent, reads and writes — and all of it
1605
+ * is drawn by these rules rather than by a frame loop, which is why watching
1606
+ * costs nothing while nothing is happening.
1607
+ *
1608
+ * Who moved is carried in HUE, not in a badge. A person is the accent the
1609
+ * whole interface already uses for "you did this"; an agent is a colder cast,
1610
+ * so a turn it took on its own is distinguishable at a glance from one you
1611
+ * directed; both at once takes both.
1612
+ */
1613
+ [data-graview-activity] {
1614
+ --graview-activity: var(--graview-accent);
1615
+ }
1616
+ [data-graview-activity="autonomous"] {
1617
+ --graview-activity: hsl(212 72% 62%);
1618
+ }
1619
+ [data-graview-activity="co-edited"] {
1620
+ --graview-activity: hsl(280 60% 66%);
1621
+ }
1622
+ [data-graview-activity="rule"] {
1623
+ --graview-activity: var(--graview-warn);
1624
+ }
1625
+
1626
+ @keyframes graview-landed {
1627
+ 0% { box-shadow: 0 0 0 0 var(--graview-activity); opacity: 0.55; }
1628
+ 30% { box-shadow: 0 0 30px 4px var(--graview-activity); opacity: 1; }
1629
+ 100% { box-shadow: 0 0 0 0 transparent; opacity: 1; }
1630
+ }
1631
+ [data-graview-view][data-graview-wrote] > * {
1632
+ animation: graview-landed 900ms cubic-bezier(0.22, 1, 0.36, 1);
1633
+ border-radius: 10px;
1634
+ }
1635
+
1636
+ /* What an agent READ is the half a diff cannot show, and the half that says
1637
+ whether to trust what it then did. Quieter than a write on purpose: it is
1638
+ evidence of attention, not of a change. */
1639
+ @keyframes graview-considered {
1640
+ 0% { outline-color: transparent; }
1641
+ 25% { outline-color: var(--graview-activity); }
1642
+ 100% { outline-color: transparent; }
1643
+ }
1644
+ [data-graview-view][data-graview-read] > :not([data-graview-natural]):not(.graview-kind-tag),
1645
+ [data-graview-view][data-graview-read] > [data-graview-natural] > * {
1646
+ outline: 1px dashed transparent;
1647
+ outline-offset: 3px;
1648
+ border-radius: 10px;
1649
+ animation: graview-considered 1800ms ease-out;
1650
+ }
1651
+
1652
+ /* A rule that has just begun to fail. It outlives the edit that caused it,
1653
+ because the problem does. */
1654
+ [data-graview-view][data-graview-broke] > * {
1655
+ box-shadow: 0 0 0 1px var(--graview-warn), 0 0 22px -6px var(--graview-warn);
1656
+ border-radius: 10px;
1657
+ }
1658
+
1659
+ /* A relation made or broken: both ends were written, so the line between
1660
+ them is the thing that changed. */
1661
+ @keyframes graview-relation {
1662
+ 0% { stroke-opacity: 0.28; stroke-width: 1.4; }
1663
+ 30% { stroke-opacity: 1; stroke-width: 3; }
1664
+ 100% { stroke-opacity: 0.28; stroke-width: 1.4; }
1665
+ }
1666
+ [data-graview-connector][data-graview-activity] {
1667
+ animation: graview-relation 1200ms cubic-bezier(0.22, 1, 0.36, 1);
1668
+ }
1669
+
1670
+ /*
1671
+ * MOTION, ASKED OF THE SYSTEM AND OVERRIDABLE BY THE PERSON.
1672
+ *
1673
+ * The system's own preference is the default and always was. The "motion"
1674
+ * setting (see readerSettings) stamps data-graview-motion on the root
1675
+ * element when — and only when — a reader overrides it: "reduce" turns the same
1676
+ * rules on for somebody whose system says otherwise, and "full" turns them
1677
+ * off for somebody whose system says reduce and who wants the movement
1678
+ * here anyway. Choosing "as your system has it" removes the attribute, so
1679
+ * the media query is what answers again.
1680
+ *
1681
+ * Written as one rule list under two selectors rather than duplicated,
1682
+ * because a stylesheet where the two copies can drift is a stylesheet where
1683
+ * they will.
1684
+ */
1685
+ @media (prefers-reduced-motion: reduce) {
1686
+ .graview-figure { transition: none; }
1687
+ ${stillness(`${motionRoot}:not([data-graview-motion='full'])`, text)}
1688
+ }
1689
+ ${stillness(`${motionRoot}[data-graview-motion='reduce']`, text)}
1690
+ `;
1691
+ }
1692
+ //# sourceMappingURL=theme.js.map