tablewalk 0.0.1

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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +553 -0
  3. package/dist/adapters/adapter.js +372 -0
  4. package/dist/adapters/connect.js +33 -0
  5. package/dist/adapters/mysql.js +951 -0
  6. package/dist/adapters/postgres.js +1000 -0
  7. package/dist/adapters/sqlite.js +781 -0
  8. package/dist/client/agent.js +262 -0
  9. package/dist/client/app.js +973 -0
  10. package/dist/client/arrange.js +254 -0
  11. package/dist/client/ask.js +133 -0
  12. package/dist/client/breakdown.js +317 -0
  13. package/dist/client/clauses.js +390 -0
  14. package/dist/client/columns.js +98 -0
  15. package/dist/client/complete.js +437 -0
  16. package/dist/client/compose.js +166 -0
  17. package/dist/client/composer.css +495 -0
  18. package/dist/client/composer.js +1972 -0
  19. package/dist/client/connections.js +234 -0
  20. package/dist/client/connmanager.js +962 -0
  21. package/dist/client/connurl.js +188 -0
  22. package/dist/client/core.js +893 -0
  23. package/dist/client/deeplink.js +270 -0
  24. package/dist/client/delete.js +144 -0
  25. package/dist/client/diagram.js +885 -0
  26. package/dist/client/dropdown.js +279 -0
  27. package/dist/client/export.js +456 -0
  28. package/dist/client/features.css +524 -0
  29. package/dist/client/findvalue.js +169 -0
  30. package/dist/client/grid.js +205 -0
  31. package/dist/client/handoff.js +153 -0
  32. package/dist/client/help.css +145 -0
  33. package/dist/client/help.js +881 -0
  34. package/dist/client/history.js +222 -0
  35. package/dist/client/index.html +116 -0
  36. package/dist/client/insert.js +151 -0
  37. package/dist/client/menu.js +160 -0
  38. package/dist/client/nested.js +255 -0
  39. package/dist/client/page.css +713 -0
  40. package/dist/client/page.js +1345 -0
  41. package/dist/client/pagebuilder.js +1222 -0
  42. package/dist/client/pagemarks.js +95 -0
  43. package/dist/client/palette.js +374 -0
  44. package/dist/client/peek.js +254 -0
  45. package/dist/client/picker.js +139 -0
  46. package/dist/client/pins.js +140 -0
  47. package/dist/client/prompt.js +129 -0
  48. package/dist/client/record.js +707 -0
  49. package/dist/client/schemaexport.js +242 -0
  50. package/dist/client/schematext.js +125 -0
  51. package/dist/client/shape.js +178 -0
  52. package/dist/client/shapecheck.js +129 -0
  53. package/dist/client/skeleton.js +139 -0
  54. package/dist/client/sql.css +126 -0
  55. package/dist/client/sql.js +398 -0
  56. package/dist/client/sqlcomplete.js +163 -0
  57. package/dist/client/sqlsaved.js +107 -0
  58. package/dist/client/style.css +2711 -0
  59. package/dist/client/summary.js +259 -0
  60. package/dist/client/table.js +1035 -0
  61. package/dist/client/template.js +539 -0
  62. package/dist/client/theme.js +74 -0
  63. package/dist/client/tour.js +324 -0
  64. package/dist/client/undo.js +105 -0
  65. package/dist/client/url.js +166 -0
  66. package/dist/client/value.js +223 -0
  67. package/dist/client/views.js +215 -0
  68. package/dist/client/virtual.js +176 -0
  69. package/dist/client/welcome.js +170 -0
  70. package/dist/client/write.js +414 -0
  71. package/dist/server/changeimpact.js +195 -0
  72. package/dist/server/connections.js +615 -0
  73. package/dist/server/constraints.js +62 -0
  74. package/dist/server/credentials.js +230 -0
  75. package/dist/server/fixture.js +199 -0
  76. package/dist/server/graph.js +194 -0
  77. package/dist/server/impact.js +48 -0
  78. package/dist/server/index.js +2204 -0
  79. package/dist/server/journal.js +173 -0
  80. package/dist/server/layouts.js +128 -0
  81. package/dist/server/mcp.js +2840 -0
  82. package/dist/server/shapeonly.js +91 -0
  83. package/dist/shared/breakdown.js +231 -0
  84. package/dist/shared/breakdowntext.js +257 -0
  85. package/dist/shared/diff.js +130 -0
  86. package/dist/shared/like.js +29 -0
  87. package/dist/shared/lint.js +149 -0
  88. package/dist/shared/order.js +133 -0
  89. package/dist/shared/page.js +932 -0
  90. package/dist/shared/query.js +831 -0
  91. package/dist/shared/recordview.js +343 -0
  92. package/dist/shared/schema.js +377 -0
  93. package/dist/shared/sqlsaved.js +67 -0
  94. package/dist/shared/view.js +981 -0
  95. package/dist/shared/viewtext.js +273 -0
  96. package/dist/shared/vocabulary.js +164 -0
  97. package/package.json +57 -0
@@ -0,0 +1,2711 @@
1
+ /* Tokens. Light is the bare :root; dark is redefined twice — once for the
2
+ OS preference (guarded so an explicit light choice still wins) and once for
3
+ the explicit stamp. Nothing outside these blocks names a colour directly. */
4
+ :root {
5
+ --bg: #fbfaf8;
6
+ --surface: #ffffff;
7
+ --surface-2: #f3f1ed;
8
+ --border: #e2ded6;
9
+ --border-strong: #cbc5b9;
10
+ --text: #24211c;
11
+ --text-dim: #6d675d;
12
+ --text-faint: #98918480;
13
+ --accent: #1d6a5a;
14
+ --accent-soft: #1d6a5a14;
15
+ --link: #17594c;
16
+ --warn: #9a4b12;
17
+ --danger: #a12d2d;
18
+ --key: #7a5cc0;
19
+ --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
20
+ --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
21
+ --radius: 6px;
22
+
23
+ /* Motion.
24
+
25
+ The built-in CSS easings are too weak to read as intentional, so the
26
+ curves are explicit. `ease-in` appears nowhere: it starts slow, delaying
27
+ the exact moment the user is watching, and an ease-out at 200ms feels
28
+ faster than an ease-in at 200ms.
29
+
30
+ Durations are short because this is a tool, not a presentation. A
31
+ dropdown at 160ms feels responsive; the same dropdown at 400ms feels
32
+ like it is thinking. Nothing here exceeds 220ms.
33
+
34
+ What is deliberately NOT animated: anything reached by keyboard, and
35
+ anything you see a hundred times a day. Arrowing through the table list
36
+ and paging through rows are instant, because animating a repeated action
37
+ makes the tool feel slower every single time. */
38
+ --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
39
+ --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
40
+ --t-press: 140ms;
41
+ --t-hover: 120ms;
42
+ --t-popover: 160ms;
43
+ --t-panel: 200ms;
44
+ }
45
+
46
+ @media (prefers-color-scheme: dark) {
47
+ :root:not([data-theme="light"]) {
48
+ --bg: #16181a;
49
+ --surface: #1c1f22;
50
+ --surface-2: #23272b;
51
+ --border: #2f343a;
52
+ --border-strong: #454c54;
53
+ --text: #e4e2dd;
54
+ --text-dim: #9aa0a6;
55
+ --text-faint: #6b727980;
56
+ --accent: #5fc9ad;
57
+ --accent-soft: #5fc9ad1a;
58
+ --link: #6fd4b8;
59
+ --warn: #e0a05c;
60
+ --danger: #e8756f;
61
+ --key: #b39ae8;
62
+ }
63
+ }
64
+
65
+ :root[data-theme="dark"] {
66
+ --bg: #16181a;
67
+ --surface: #1c1f22;
68
+ --surface-2: #23272b;
69
+ --border: #2f343a;
70
+ --border-strong: #454c54;
71
+ --text: #e4e2dd;
72
+ --text-dim: #9aa0a6;
73
+ --text-faint: #6b727980;
74
+ --accent: #5fc9ad;
75
+ --accent-soft: #5fc9ad1a;
76
+ --link: #6fd4b8;
77
+ --warn: #e0a05c;
78
+ --danger: #e8756f;
79
+ --key: #b39ae8;
80
+ }
81
+
82
+ * { box-sizing: border-box; }
83
+
84
+ body {
85
+ margin: 0;
86
+ background: var(--bg);
87
+ color: var(--text);
88
+ font: 14px/1.5 var(--sans);
89
+ height: 100vh;
90
+ display: flex;
91
+ flex-direction: column;
92
+ }
93
+
94
+ /* ---------- top bar ---------- */
95
+
96
+ .topbar {
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 16px;
100
+ padding: 8px 14px;
101
+ background: var(--surface);
102
+ border-bottom: 1px solid var(--border);
103
+ flex: none;
104
+ }
105
+
106
+ /* The wordmark is a home link, so it has to look pressable without becoming
107
+ a button in the visual sense — no border, no fill, just a hit area. */
108
+ .brand {
109
+ display: flex; align-items: center; gap: 8px; flex: none;
110
+ /* Colour reset explicitly, not just the background. The base button rule
111
+ sets `color: var(--bg)` to sit on an accent fill; unsetting only the fill
112
+ left near-white text on a white bar, visible solely on hover. */
113
+ background: none; border: none; border-radius: var(--radius);
114
+ color: var(--text);
115
+ padding: 4px 6px; margin-left: -6px; cursor: pointer;
116
+ font: inherit;
117
+ }
118
+ .brand:hover .brand-name { color: var(--accent); }
119
+ .brand:hover .brand-mark { border-color: var(--accent); }
120
+
121
+ /* Two linked nodes, which is the whole idea of the tool in 14 pixels. */
122
+ .brand-mark {
123
+ width: 14px; height: 14px; border-radius: 50%;
124
+ border: 2px solid var(--accent);
125
+ position: relative;
126
+ }
127
+ .brand-mark::after {
128
+ content: ""; position: absolute; left: 12px; top: 3px;
129
+ width: 10px; height: 2px; background: var(--accent);
130
+ }
131
+
132
+ .brand-name { font-weight: 620; letter-spacing: -0.01em; color: var(--text); font-size: 14px; }
133
+ /* The connection name, which is also the button that opens Connections.
134
+ Quiet by default — it reads as the label it is — and only takes a box on
135
+ hover, because a permanent button here would compete with the query bar
136
+ for the eye every time the page is looked at. */
137
+ .brand-db {
138
+ color: var(--text-dim);
139
+ font-family: var(--mono);
140
+ font-size: 12px;
141
+ padding: 4px 6px;
142
+ border: 1px solid transparent;
143
+ border-radius: var(--radius);
144
+ background: none;
145
+ cursor: pointer;
146
+ white-space: nowrap;
147
+ }
148
+ .brand-db:hover { color: var(--text); border-color: var(--border-strong); }
149
+ .brand-db:focus-visible { color: var(--text); border-color: var(--border-strong); }
150
+ /* Before a connection is named there is nothing to click, and an empty box
151
+ beside the wordmark reads as a rendering fault. */
152
+ .brand-db:empty { display: none; }
153
+
154
+ .querybar { display: flex; gap: 6px; flex: 1; min-width: 0; }
155
+
156
+ #query {
157
+ flex: 1; min-width: 0;
158
+ padding: 7px 10px;
159
+ font: 13px/1.4 var(--mono);
160
+ color: var(--text);
161
+ background: var(--bg);
162
+ border: 1px solid var(--border-strong);
163
+ border-radius: var(--radius);
164
+ }
165
+ #query:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: transparent; }
166
+
167
+ button {
168
+ font: inherit; font-size: 13px;
169
+ padding: 7px 12px;
170
+ border-radius: var(--radius);
171
+ border: 1px solid transparent;
172
+ background: var(--accent); color: var(--bg);
173
+ cursor: pointer;
174
+ }
175
+ button.ghost { background: transparent; color: var(--text-dim); border-color: var(--border-strong); }
176
+ button.ghost[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
177
+ button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
178
+ @media (prefers-reduced-motion: no-preference) and (hover: hover) and (pointer: fine) {
179
+ /* Only transform and opacity, which skip layout and paint. Confined to
180
+ deliberate controls — a row you click a hundred times a day should not
181
+ spring. */
182
+ .topbar button, dialog.confirm button, .view-save {
183
+ transition: transform var(--t-press) var(--ease-out),
184
+ background var(--t-hover) ease,
185
+ border-color var(--t-hover) ease,
186
+ color var(--t-hover) ease;
187
+ }
188
+ .topbar button:active, dialog.confirm button:active { transform: scale(0.97); }
189
+ }
190
+
191
+ /* The pane, so the control that edits the statement sits in its corner rather
192
+ than on a row of its own. A persistent full-width row for one button is a
193
+ line of chrome between the query bar and the results, on every page, for an
194
+ action taken occasionally. */
195
+ .explain-pane { position: relative; flex: none; }
196
+ .explain-pane[hidden] { display: none; }
197
+ .explain-edit {
198
+ position: absolute; top: 6px; right: 12px;
199
+ padding: 2px 9px; border-radius: 5px;
200
+ background: var(--surface); border: 1px solid var(--border);
201
+ color: var(--text-dim); cursor: pointer;
202
+ font: 10.5px var(--mono);
203
+ opacity: 0;
204
+ }
205
+ /* Revealed on approach, and always for the keyboard — an affordance nobody can
206
+ tab to is one that does not exist for anyone not using a mouse. */
207
+ .explain-pane:hover .explain-edit,
208
+ .explain-edit:focus-visible { opacity: 1; }
209
+ .explain-edit:hover { color: var(--accent); border-color: var(--accent); }
210
+
211
+ .explain {
212
+ margin: 0; padding: 10px 14px;
213
+ font: 12px/1.55 var(--mono);
214
+ background: var(--surface-2);
215
+ border-bottom: 1px solid var(--border);
216
+ color: var(--text-dim);
217
+ white-space: pre; overflow-x: auto;
218
+ flex: none;
219
+ }
220
+
221
+ .errors {
222
+ padding: 8px 14px; flex: none;
223
+ background: color-mix(in srgb, var(--danger) 10%, var(--surface));
224
+ border-bottom: 1px solid var(--border);
225
+ color: var(--danger);
226
+ font-size: 13px;
227
+ }
228
+ .errors p { margin: 2px 0; }
229
+
230
+ /* ---------- layout ---------- */
231
+
232
+ .layout { display: flex; flex: 1; min-height: 0; }
233
+
234
+ .sidebar {
235
+ width: 232px; flex: none;
236
+ border-right: 1px solid var(--border);
237
+ background: var(--surface);
238
+ display: flex; flex-direction: column;
239
+ min-height: 0;
240
+ }
241
+ #table-filter {
242
+ margin: 8px; padding: 6px 8px;
243
+ font: 12px var(--sans); color: var(--text);
244
+ background: var(--bg);
245
+ border: 1px solid var(--border); border-radius: var(--radius);
246
+ }
247
+ #table-list { list-style: none; margin: 0; padding: 0 6px 12px; overflow-y: auto; flex: 1; }
248
+ #table-list li { margin-bottom: 1px; }
249
+ #table-list button {
250
+ width: 100%; text-align: left;
251
+ background: transparent; color: var(--text);
252
+ border: none; border-radius: var(--radius);
253
+ padding: 5px 8px;
254
+ font: 12.5px/1.35 var(--mono);
255
+ display: flex; justify-content: space-between; gap: 8px; align-items: baseline;
256
+ }
257
+ #table-list button:hover { background: var(--surface-2); }
258
+ #table-list button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
259
+ #table-list .rows { color: var(--text-dim); font-size: 11px; font-variant-numeric: tabular-nums; }
260
+ #table-list .view-tag { color: var(--key); font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; }
261
+
262
+ /* `scrollbar-gutter: stable` reserves the scrollbar's width whether or not
263
+ it is showing, so content keeps the same right-hand gutter either way —
264
+ without it the bar sits *in* the padding and the page reads as flush
265
+ against the window on that side only. */
266
+ .content {
267
+ flex: 1; min-width: 0; overflow: auto; display: flex; flex-direction: column;
268
+ scrollbar-gutter: stable;
269
+ }
270
+ /* The record grid fills the height so its divider runs the full way down,
271
+ rather than stopping wherever the shorter column happens to end. */
272
+ .content > .record { flex: 1; }
273
+
274
+ .empty { margin: auto; max-width: 30rem; padding: 32px; text-align: center; }
275
+ .empty h1 { font-size: 20px; font-weight: 600; letter-spacing: -0.015em; margin: 0 0 8px; }
276
+ .empty p { color: var(--text-dim); margin: 0; }
277
+
278
+ /* ---------- breadcrumb ---------- */
279
+
280
+ .crumbs {
281
+ display: flex; align-items: baseline; gap: 1px; flex-wrap: wrap;
282
+ padding: 8px 14px;
283
+ border-bottom: 1px solid var(--border);
284
+ background: var(--surface);
285
+ font-size: 12.5px;
286
+ position: sticky; top: 0; z-index: 2;
287
+ }
288
+ .crumbs button {
289
+ background: none; border: none; padding: 2px 5px;
290
+ color: var(--link); font: inherit; font-family: var(--mono);
291
+ cursor: pointer; border-radius: 3px;
292
+ }
293
+ .crumbs button:hover { background: var(--surface-2); }
294
+ /* The chevron is the only separator, and it is set lighter and roomier than
295
+ the crumbs so the eye reads it as structure rather than content. */
296
+ .crumbs .sep {
297
+ color: var(--text-faint);
298
+ font-size: 11px;
299
+ padding: 0 3px;
300
+ }
301
+ .crumbs .current {
302
+ color: var(--text); font-weight: 500;
303
+ font-family: var(--mono); padding: 2px 5px;
304
+ }
305
+
306
+ /* ---------- results table ---------- */
307
+
308
+ /* ---------- tabs ---------- */
309
+
310
+ .tabs { display: flex; gap: 2px; padding: 8px 14px 0; border-bottom: 1px solid var(--border); }
311
+ .tab {
312
+ background: none; border: none; border-bottom: 2px solid transparent;
313
+ color: var(--text-dim); padding: 5px 10px 7px;
314
+ font: 12.5px var(--sans); border-radius: 0; cursor: pointer;
315
+ }
316
+ .tab:hover { color: var(--text); }
317
+ .tab[aria-selected="true"] { color: var(--accent); border-bottom-color: var(--accent); }
318
+
319
+ /* Wider than it was: with the sidebar on the left, 14px of gutter on the
320
+ right reads as none at all beside a panel edge. */
321
+ .panel-pad { padding: 16px 20px; }
322
+
323
+ .ddl-note { margin: 12px 14px 0; color: var(--text-dim); font-size: 12px; }
324
+ .ddl-note strong { color: var(--text); font-weight: 600; }
325
+ .ddl {
326
+ margin: 8px 14px 16px; padding: 12px 14px;
327
+ background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
328
+ font: 12.5px/1.6 var(--mono); color: var(--text);
329
+ white-space: pre; overflow-x: auto;
330
+ }
331
+
332
+ /* ---------- diagram ---------- */
333
+
334
+ /* ---------- diagram: viewport, zoom, focus ---------- */
335
+
336
+ /* No width cap: the diagram is a viewport now, and every pixel of panel it
337
+ does not use is a pixel of schema the reader has to pan to instead. */
338
+ .diagram { max-width: none; }
339
+
340
+ /* A window onto a drawing larger than the window.
341
+
342
+ Given a height rather than left to size to its content: a viewport that
343
+ grows with its content is not a viewport, and the whole point is that the
344
+ picture can be bigger than the space it is read in. */
345
+ .diagram-viewport {
346
+ position: relative;
347
+ height: min(72vh, 760px);
348
+ border: 1px solid var(--border);
349
+ border-radius: 8px;
350
+ background: var(--bg);
351
+ overflow: hidden;
352
+ cursor: grab;
353
+ touch-action: none;
354
+ }
355
+ .diagram-viewport.dragging { cursor: grabbing; }
356
+ .diagram-viewport svg { display: block; }
357
+
358
+ .zoom-controls {
359
+ position: absolute; right: 10px; bottom: 10px;
360
+ display: flex; align-items: center; gap: 4px;
361
+ padding: 4px; border-radius: 7px;
362
+ background: var(--surface); border: 1px solid var(--border);
363
+ box-shadow: 0 2px 10px rgb(0 0 0 / 0.06);
364
+ }
365
+ .zoom-controls button { padding: 2px 8px; font-size: 12px; }
366
+ .zoom-readout {
367
+ min-width: 3.4rem; text-align: center;
368
+ font: 11px var(--mono); color: var(--text-dim);
369
+ font-variant-numeric: tabular-nums;
370
+ }
371
+
372
+ .diagram-head {
373
+ display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
374
+ margin-bottom: 12px;
375
+ }
376
+ .diagram-head .diagram-title { margin: 0; }
377
+
378
+ .diagram-focus {
379
+ display: flex; align-items: center; gap: 6px;
380
+ margin-left: auto; justify-content: flex-end;
381
+ /* wrap-reverse so an overflowing row of chips grows upwards. Plain `wrap`
382
+ would push the field itself onto a second line, which is the same
383
+ moving-target problem in the other axis. */
384
+ flex-wrap: wrap-reverse;
385
+ }
386
+ .diagram-focus .picker { flex: none; }
387
+
388
+ /* A field with a menu we draw ourselves. See picker.js for why. */
389
+ .picker { position: relative; }
390
+ .picker-input {
391
+ width: 15rem; max-width: 100%; padding: 5px 9px;
392
+ border: 1px solid var(--border); border-radius: 6px;
393
+ background: var(--bg); color: var(--text);
394
+ font: 12px var(--mono);
395
+ }
396
+ .picker-input:focus { outline: none; border-color: var(--accent); }
397
+ .picker-list {
398
+ z-index: 40; max-height: 17rem; overflow-y: auto;
399
+ padding: 4px;
400
+ background: var(--surface);
401
+ border: 1px solid var(--border-strong); border-radius: 8px;
402
+ box-shadow: 0 8px 28px rgb(0 0 0 / 0.16);
403
+ }
404
+ .picker-option {
405
+ display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
406
+ padding: 6px 9px; border-radius: 5px; cursor: pointer;
407
+ }
408
+ .picker-option.active { background: var(--accent-soft); }
409
+ .picker-label { font: 12.5px var(--mono); color: var(--text); }
410
+ .picker-option.active .picker-label { color: var(--accent); }
411
+ .picker-detail { font: 10.5px var(--mono); color: var(--text-faint); }
412
+ .picker-empty { padding: 10px; margin: 0; font-size: 12px; color: var(--text-dim); }
413
+ .diagram-chip {
414
+ display: inline-flex; align-items: center; gap: 6px;
415
+ padding: 4px 8px; border-radius: 999px;
416
+ border: 1px solid var(--accent); background: var(--accent-soft);
417
+ color: var(--accent); font: 11.5px var(--mono); cursor: pointer;
418
+ }
419
+ .diagram-chip .chip-x { opacity: 0.6; }
420
+ .diagram-chip:hover .chip-x { opacity: 1; }
421
+ .diagram-clear { font-size: 11.5px; }
422
+
423
+ /* ---------- command palette ---------- */
424
+
425
+ .palette-backdrop {
426
+ /* Above everything it can be opened from.
427
+
428
+ ⌘K is bound at the document with capture, so it fires whatever is on
429
+ screen — and at 60 it opened *underneath* the SQL panel (75), the
430
+ connection manager (80) and the page builder (85). The result was a
431
+ focused text field nobody could see, swallowing keystrokes meant for the
432
+ surface in front of it. Whatever the palette can open over, it opens
433
+ above. */
434
+ position: fixed; inset: 0; z-index: 100;
435
+ background: rgb(0 0 0 / 0.28);
436
+ display: flex; justify-content: center;
437
+ padding-top: 12vh;
438
+ }
439
+ .palette {
440
+ width: min(620px, 92vw); max-height: 62vh;
441
+ display: flex; flex-direction: column;
442
+ background: var(--surface);
443
+ border: 1px solid var(--border-strong);
444
+ border-radius: 10px;
445
+ box-shadow: 0 18px 60px rgb(0 0 0 / 0.28);
446
+ overflow: hidden;
447
+ }
448
+ .palette-input {
449
+ padding: 14px 16px;
450
+ border: none; border-bottom: 1px solid var(--border);
451
+ background: none; color: var(--text);
452
+ font: 14px var(--mono);
453
+ }
454
+ /* The outline is dropped because the palette's own frame is the focus
455
+ indicator while it is the only thing focusable — but Tab can move to a
456
+ result and back, and coming back to an input with no indicator at all is
457
+ the caret being somewhere nobody can see. */
458
+ .palette-input:focus { outline: none; border-bottom-color: var(--accent); }
459
+ .palette-list { overflow-y: auto; padding: 6px; }
460
+ .palette-item {
461
+ display: grid; grid-template-columns: 4.6rem 1fr auto;
462
+ align-items: baseline; gap: 10px;
463
+ padding: 7px 10px; border-radius: 6px; cursor: pointer;
464
+ }
465
+ .palette-item.active { background: var(--accent-soft); }
466
+ .palette-kind {
467
+ font: 10px var(--mono); text-transform: uppercase; letter-spacing: 0.06em;
468
+ color: var(--text-faint);
469
+ }
470
+ .palette-kind.kind-view, .palette-kind.kind-filter { color: var(--link); }
471
+ .palette-kind.kind-recent { color: var(--text-faint); }
472
+ .palette-kind.kind-action { color: var(--accent); }
473
+ .palette-kind.kind-page { color: var(--accent); }
474
+ .palette-kind.kind-sql { color: var(--key); }
475
+ .palette-label {
476
+ font: 13px var(--mono); color: var(--text);
477
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
478
+ }
479
+ .palette-item.active .palette-label { color: var(--accent); }
480
+ .palette-detail { font: 11px var(--mono); color: var(--text-faint); }
481
+ .palette-empty { padding: 16px; margin: 0; font-size: 12.5px; color: var(--text-dim); }
482
+
483
+ /* ---------- the sidebar toggle ---------- */
484
+
485
+ /* Header controls that are not the primary action: an icon's worth of space
486
+ each, no outline until you reach for them. The outlined-rectangle treatment
487
+ is reserved for Run and for the mode, which are the two things in the header
488
+ worth reading every time. */
489
+ .icon-button {
490
+ padding: 5px 9px; border-radius: 6px;
491
+ background: none; border: 1px solid transparent;
492
+ color: var(--text-dim); cursor: pointer;
493
+ font: 11px var(--mono);
494
+ }
495
+ .icon-button:hover { color: var(--accent); border-color: var(--border); background: var(--surface); }
496
+ .icon-button[aria-expanded="true"],
497
+ .icon-button[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
498
+
499
+ .more-list {
500
+ z-index: 70; min-width: 15rem; padding: 5px;
501
+ background: var(--surface);
502
+ border: 1px solid var(--border-strong); border-radius: 8px;
503
+ box-shadow: 0 10px 34px rgb(0 0 0 / 0.18);
504
+ }
505
+ .more-item {
506
+ display: grid; grid-template-columns: 1.1rem 1fr auto;
507
+ align-items: baseline; gap: 8px;
508
+ padding: 7px 9px; border-radius: 5px; cursor: pointer;
509
+ font: 12.5px var(--mono); color: var(--text);
510
+ }
511
+ .more-item:hover, .more-item:focus { background: var(--accent-soft); outline: none; }
512
+ .more-item:focus .more-label { color: var(--accent); }
513
+ .more-tick { color: var(--accent); font-size: 11px; }
514
+ .more-detail { font-size: 10.5px; color: var(--text-faint); }
515
+
516
+ /* The collapse control sits in the sidebar, on the filter field's row. */
517
+ .sidebar-filter-row { display: flex; align-items: center; gap: 6px; margin: 8px; }
518
+ .sidebar-filter-row #table-filter { margin: 0; flex: 1; min-width: 0; }
519
+ /* An icon inside a flex button is a flex item, and shrinks: the gear went to
520
+ two pixels wide while keeping its full height, which reads as a stray
521
+ vertical line rather than a control. The `width` attribute on an `<svg>` is
522
+ only a hint once flexbox is doing the sizing. */
523
+ .conn-manage svg,
524
+ .sidebar-collapse svg,
525
+ .sidebar-reveal svg { flex: none; display: block; }
526
+
527
+ /* `--text-faint` is a mid-grey at half alpha — the token for a rule between
528
+ things, not for a control somebody has to find. These are the two controls
529
+ a first-time reader is least likely to notice and most likely to want, so
530
+ they are drawn at the same strength as the text they sit beside, with a hit
531
+ area big enough to be one. */
532
+ .sidebar-collapse {
533
+ flex: none; display: inline-flex; align-items: center; justify-content: center;
534
+ width: 28px; height: 28px; line-height: 1;
535
+ background: none; border: 1px solid transparent; border-radius: 6px;
536
+ color: var(--text-dim); cursor: pointer;
537
+ }
538
+ .sidebar-collapse:hover { color: var(--text); border-color: var(--border); background: var(--surface-2); }
539
+
540
+ /* And the way back sits where the sidebar was. */
541
+ .sidebar-reveal {
542
+ display: none;
543
+ align-self: flex-start; flex: none;
544
+ /* Room on every side: it sat hard against the window edge with its focus
545
+ ring clipped by it, and hard against the content on the other. */
546
+ margin: 10px 10px 0; line-height: 1;
547
+ /* No `display` here: the rule above says `none`, and the one that reveals
548
+ this button is the one that decides how it is laid out. Setting
549
+ `inline-flex` in the base rule overrode that `none` and left the button
550
+ on screen beside the sidebar it exists to bring back. */
551
+ align-items: center; justify-content: center;
552
+ width: 28px; height: 28px;
553
+ background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
554
+ color: var(--text-dim); cursor: pointer;
555
+ }
556
+ .sidebar-reveal:hover { color: var(--text); border-color: var(--accent); }
557
+ /* Drawn inside its own box, so the ring cannot be cut off by the edge of the
558
+ window the button is pinned to. */
559
+ .sidebar-reveal:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
560
+
561
+ body.sidebar-hidden .sidebar { display: none; }
562
+ body.sidebar-hidden .sidebar-reveal { display: inline-flex; }
563
+
564
+ .diagram svg { display: block; overflow: visible; }
565
+ /* Opened nodes make the picture wider than the panel; it scrolls rather than
566
+ shrinking the type to fit. */
567
+ .node > rect { fill: var(--surface); stroke: var(--border-strong); stroke-width: 1; }
568
+ .node text { font: 12px var(--mono); fill: var(--text); }
569
+ .node-head { cursor: pointer; }
570
+ .node-head-hit { fill: transparent; stroke: none; }
571
+ .node:hover > rect { stroke: var(--accent); }
572
+ .node-head:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
573
+ .node-focus > rect { fill: var(--accent-soft); stroke: var(--accent); stroke-width: 1.5; }
574
+ .node-focus .node-head text { fill: var(--accent); }
575
+ .node-head text { font-weight: 600; }
576
+
577
+ /* The column list. Quieter than the table name it sits under — it is there to
578
+ be scanned for a name you already have in mind, not read top to bottom. */
579
+ .node-rule { stroke: var(--border); stroke-width: 1; }
580
+ .node-field { font-size: 11px; fill: var(--text-dim); }
581
+ .node-field.is-key { fill: var(--text); }
582
+ .node-field.is-ref { fill: var(--accent); }
583
+ .node-type { font-size: 10px; fill: var(--text-faint); }
584
+ .node-more { cursor: pointer; }
585
+ .node-more-hit { fill: transparent; stroke: none; }
586
+ .node-more text { font-size: 10.5px; fill: var(--text-faint); }
587
+ .node-more:hover text { fill: var(--accent); }
588
+ .node-more:hover .node-more-hit { fill: var(--accent-soft); }
589
+ .node-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
590
+ .edge { fill: none; stroke: var(--border-strong); stroke-width: 1.25; }
591
+ .edge-head { fill: var(--border-strong); }
592
+ .edge-label { font: 10.5px var(--mono); fill: var(--text-dim); }
593
+ .diagram-legend { margin: 12px 0 0; font-size: 11.5px; color: var(--text-faint); font-family: var(--mono); }
594
+ .diagram-legend span { color: var(--text-dim); }
595
+
596
+ .result-head {
597
+ display: flex; align-items: baseline; gap: 12px;
598
+ padding: 10px 14px 6px;
599
+ color: var(--text-dim); font-size: 12px;
600
+ }
601
+ .result-head h2 { margin: 0; font: 600 15px var(--mono); color: var(--text); letter-spacing: -0.01em; }
602
+ .result-head .stat { font-variant-numeric: tabular-nums; }
603
+
604
+ /* No horizontal padding on the scrollport.
605
+
606
+ `position: sticky; left: 0` pins to the *padding* edge, so a scrollport with
607
+ 14px of left padding leaves a 14px strip that pinned cells do not cover and
608
+ scrolled content passes under — the tail of a column showing through beside
609
+ the frozen one. The inset moves into the edge cells, where it is the same
610
+ 14px and covers nothing. */
611
+ .table-wrap { overflow-x: auto; padding: 0 0 14px; }
612
+ th:first-child, td:first-child { padding-left: 14px; }
613
+ th:last-child, td:last-child { padding-right: 14px; }
614
+ th:first-child .th-sort { padding-left: 0; }
615
+
616
+ /* Columns frozen against a sideways scroll. The `left` on each is measured
617
+ after layout — see applyPins — because it is the width of everything before
618
+ it, which the browser knows and this file cannot. */
619
+ .pinned {
620
+ position: sticky;
621
+ z-index: 1;
622
+ background: var(--bg);
623
+ }
624
+ thead .pinned { z-index: 3; background: var(--surface); }
625
+ tbody tr:hover .pinned { background: var(--accent-soft); }
626
+ /* One seam, on the rightmost frozen column, rather than a line per column. */
627
+ .pinned-edge { box-shadow: 1px 0 0 var(--border-strong); }
628
+ .table-wrap:not([data-scrolled]) .pinned-edge { box-shadow: none; }
629
+ table { border-collapse: collapse; width: 100%; font: 12.5px var(--mono); }
630
+ th, td {
631
+ text-align: left; padding: 5px 10px;
632
+ border-bottom: 1px solid var(--border);
633
+ white-space: nowrap; max-width: 30rem;
634
+ overflow: hidden; text-overflow: ellipsis;
635
+ }
636
+ th {
637
+ position: sticky; top: 0;
638
+ /* Above the pinned body cells, which carry `z-index: 1` so they float over
639
+ the columns they freeze past. Without this, scrolling down slid the
640
+ frozen `id` column over its own heading — two sticky things, both
641
+ stacked, and the one that should win by being the header did not. */
642
+ z-index: 2;
643
+ background: var(--surface); color: var(--text-dim);
644
+ font-weight: 600; font-size: 11px;
645
+ text-transform: uppercase; letter-spacing: 0.05em;
646
+ border-bottom: 1px solid var(--border-strong);
647
+ padding: 0;
648
+ }
649
+ th.sorted { color: var(--accent); border-bottom-color: var(--accent); }
650
+
651
+ /* The header splits into two controls: the label sorts, the caret filters.
652
+ One click target doing both would make every attempt to filter a sort. */
653
+ .th-sort {
654
+ background: none; border: none; border-radius: 0;
655
+ color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit;
656
+ padding: 5px 4px 5px 10px; cursor: pointer; white-space: nowrap;
657
+ }
658
+ .th-sort:hover { color: var(--accent); }
659
+ .th-arrow { color: var(--accent); }
660
+ /* Not disabled-looking, just not offered: the column reads normally, it simply
661
+ does not respond to a click. Greying the label out would say the column
662
+ itself is unavailable, which is not what is being said. */
663
+ .th-sort.not-sortable { cursor: default; opacity: 1; }
664
+ .th-sort.not-sortable:hover { color: inherit; }
665
+ /* Marked unavailable rather than disabled, so the sentence saying why is
666
+ reachable by keyboard — see the header in table.js. The browser no longer
667
+ greys it for us, so it is greyed here. */
668
+ .th-sort[aria-disabled="true"] { cursor: default; color: var(--text-faint); }
669
+ .th-sort[aria-disabled="true"]:hover { color: var(--text-faint); }
670
+ /* The default order is shown, not announced: an arrow the same shape as a
671
+ chosen sort, drawn faintly enough to read as "this happened" rather than
672
+ "you did this". */
673
+ .sorted-default .th-arrow { color: var(--text-faint); }
674
+ /* An estimate is a button, because there is something to do about it. */
675
+ .stat-estimate {
676
+ background: none; border: 1px solid transparent; border-radius: 5px;
677
+ padding: 1px 6px; cursor: pointer;
678
+ font: inherit; color: var(--key);
679
+ }
680
+ .stat-estimate:hover { border-color: var(--key); background: color-mix(in srgb, var(--key) 10%, transparent); }
681
+
682
+ .stat-order {
683
+ background: none; border: 1px solid transparent; border-radius: 5px;
684
+ padding: 1px 6px; cursor: pointer;
685
+ font: inherit; color: var(--text-faint);
686
+ }
687
+ .stat-order:hover { color: var(--accent); border-color: var(--border); }
688
+ .th-filter {
689
+ background: none; border: none; border-radius: 3px;
690
+ color: var(--text-faint); cursor: pointer;
691
+ padding: 4px 8px 4px 2px; font-size: 11px; line-height: 1;
692
+ }
693
+ .th-filter:hover { color: var(--accent); background: var(--accent-soft); }
694
+ .align-right .th-sort { padding-left: 4px; }
695
+
696
+ .th-menu {
697
+ position: fixed; z-index: 40;
698
+ min-width: 15rem; max-width: 24rem; max-height: 20rem; overflow-y: auto;
699
+ background: var(--surface);
700
+ border: 1px solid var(--border-strong); border-radius: 8px;
701
+ box-shadow: 0 6px 20px rgb(0 0 0 / 0.16);
702
+ padding: 4px;
703
+ }
704
+ @media (prefers-reduced-motion: no-preference) {
705
+ .th-menu {
706
+ transform-origin: top center;
707
+ transition: opacity var(--t-popover) var(--ease-out), transform var(--t-popover) var(--ease-out);
708
+ @starting-style { opacity: 0; transform: scale(0.97) translateY(-4px); }
709
+ }
710
+ }
711
+ .th-menu-item {
712
+ width: 100%; display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
713
+ background: none; border: none; border-radius: 5px;
714
+ padding: 5px 8px; cursor: pointer; text-align: left;
715
+ text-transform: none; letter-spacing: 0;
716
+ }
717
+ .th-menu-item:hover { background: var(--surface-2); }
718
+ .th-menu-label { font: 12.5px var(--mono); color: var(--text); }
719
+ .th-menu-typed .th-menu-label { color: var(--link); }
720
+ .th-menu-sep { height: 1px; background: var(--border); margin: 4px 2px; }
721
+ .th-menu-cond { font: 10.5px var(--mono); color: var(--text-faint); flex: none; }
722
+ th .pk { color: var(--key); }
723
+ th .fk { color: var(--accent); }
724
+ /* Only where a click goes somewhere. A pointer over a row that does nothing
725
+ advertises an action the row does not have. */
726
+ tbody tr { cursor: default; }
727
+ tbody tr.clickable { cursor: pointer; }
728
+ tbody tr.clickable:hover { background: var(--accent-soft); }
729
+
730
+ /* Alignment is set per column by the renderer and applied to the header too,
731
+ so a label always sits over its own values. */
732
+ .align-left { text-align: left; }
733
+ .align-right { text-align: right; }
734
+ .align-center { text-align: center; }
735
+
736
+ td.mono { font-family: var(--mono); }
737
+ td.num { font-variant-numeric: tabular-nums; }
738
+ td.null { color: var(--text-faint); font-style: italic; }
739
+ td.bool .yes { color: var(--accent); }
740
+ /* `false` is a value, and was drawn in the token for absence — the same
741
+ `--text-faint` as `null`, a mid-grey at half alpha, with only an italic to
742
+ tell them apart. In a database browser those are two very different facts
743
+ about a row, and the negative one was the harder of the two to see at all.
744
+ It reads as present-and-negative now; `null` keeps the faint italic,
745
+ because absence is the thing that should recede. */
746
+ td.bool .no { color: var(--text-dim); }
747
+ td.reflink { color: var(--link); }
748
+ td .ref { color: var(--link); text-decoration: underline; text-decoration-style: dotted; }
749
+
750
+ /* Paging sits on the right, under the end of the table, where the eye
751
+ already is after reading a row across. */
752
+ .pager {
753
+ display: flex; gap: 12px; align-items: center; justify-content: space-between;
754
+ padding: 0 14px 16px; color: var(--text-dim); font-size: 12px;
755
+ flex-wrap: wrap;
756
+ }
757
+ /* How you are reading the table, on the left; where you are in it, on the
758
+ right. The size is not a step through the result, so it does not sit among
759
+ the buttons that are. */
760
+ .pager-size { display: flex; gap: 6px; align-items: center; }
761
+ /* Narrow: the list is four two-or-three-digit numbers, and the shared control
762
+ is sized for connection names. */
763
+ .pager-size .dd { min-width: 0; }
764
+ .pager-size .dd-button { padding: 3px 6px; }
765
+ .pager-size .dd-list { min-width: 0; }
766
+ .pager-steps { display: flex; gap: 8px; align-items: center; }
767
+ .pager .page-label { display: flex; gap: 6px; align-items: center; }
768
+ /* Wide enough for four digits without growing as the number does — a control
769
+ that resizes while you read it is a control that moves under the cursor. */
770
+ .page-input {
771
+ width: 5.5ch; text-align: right;
772
+ font: inherit; font-family: var(--mono);
773
+ color: var(--text); background: var(--bg);
774
+ border: 1px solid var(--border-strong); border-radius: var(--radius);
775
+ padding: 3px 4px;
776
+ }
777
+ /* A step that cannot be taken says so by looking unavailable rather than by
778
+ vanishing: buttons that come and go make the row jump as you page. */
779
+ .pager .ghost[disabled] { opacity: 0.4; cursor: default; }
780
+
781
+ /* ---------- record view ---------- */
782
+
783
+ /* The record is one surface, not a tray of cards.
784
+
785
+ Rounded panels floating in padding read as app chrome; the result grid
786
+ beside them is edge-to-edge with a hairline header, and the two views of
787
+ the same database should not look like they came from different tools. The
788
+ panels now sit flush, divided by a rule, with the same sticky header
789
+ treatment the grid uses. */
790
+ .record {
791
+ display: grid; gap: 0;
792
+ /* The reference panel needs a fixed-ish column, not a proportional one: its
793
+ content is a handful of short rows however wide the window is, so giving
794
+ it a third of a large screen leaves a column of nothing beside a field
795
+ list that has to scroll. */
796
+ grid-template-columns: minmax(0, 1fr) minmax(19rem, 24rem);
797
+ align-items: stretch;
798
+ }
799
+ @media (max-width: 900px) { .record { grid-template-columns: minmax(0, 1fr); } }
800
+
801
+ .panel { background: var(--surface); border-right: 1px solid var(--border); min-width: 0; }
802
+ .panel:last-child { border-right: none; }
803
+ @media (max-width: 900px) {
804
+ .panel { border-right: none; border-bottom: 1px solid var(--border); }
805
+ }
806
+ .panel > h3 {
807
+ margin: 0; padding: 10px 14px;
808
+ font: 600 10.5px var(--sans); text-transform: uppercase; letter-spacing: 0.09em;
809
+ color: var(--text-dim);
810
+ background: var(--surface);
811
+ border-bottom: 1px solid var(--border);
812
+ position: sticky; top: 0; z-index: 1;
813
+ }
814
+
815
+ /* ---------- record fields ----------
816
+
817
+ Grouped into sections, because a twenty-seven column table as one flat list
818
+ is technically complete and practically unreadable: identity, links, values
819
+ and bookkeeping all carry equal weight, so nothing carries any. */
820
+
821
+ .field-groups { padding: 12px 0 18px; }
822
+ .field-group-plain { padding-top: 4px; }
823
+ /* Divided by space, not by rules.
824
+
825
+ Every section already announces itself with a heading, so a border on top
826
+ of that is the same boundary drawn twice — and with five sections the
827
+ panel reads as a stack of boxes rather than one record. Whitespace is the
828
+ quieter separator and the heading does the work. */
829
+ .field-group + .field-group { margin-top: 24px; }
830
+ .field-group-head {
831
+ width: 100%; display: flex; align-items: baseline; gap: 7px;
832
+ background: none; border: none; border-radius: 0;
833
+ padding: 2px 14px 8px;
834
+ font: 600 10.5px var(--sans); text-transform: uppercase; letter-spacing: 0.09em;
835
+ color: var(--text-dim); cursor: pointer; text-align: left;
836
+ }
837
+ .field-group-head:hover { color: var(--text); }
838
+ .field-group-caret { font-size: 9px; color: var(--text-faint); }
839
+ .field-group-count {
840
+ margin-left: auto; font-variant-numeric: tabular-nums;
841
+ color: var(--text-faint); font-size: 10.5px; letter-spacing: 0;
842
+ }
843
+ .field-group.collapsed .fields { display: none; }
844
+ .field-group.collapsed .field-group-caret { transform: rotate(-90deg); }
845
+ @media (prefers-reduced-motion: no-preference) {
846
+ .field-group-caret { transition: transform 140ms ease; display: inline-block; }
847
+ }
848
+
849
+ /* Fields flow into as many columns as fit.
850
+
851
+ A twenty-seven column record in one narrow column is a scroll with empty
852
+ space beside it. `auto-fill` with a minimum means the layout answers the
853
+ question itself: wide window, two or three columns; narrow window, one.
854
+ No breakpoint to keep in step with the panel's own width. */
855
+ .fields {
856
+ margin: 0; padding: 0 0 2px;
857
+ display: grid;
858
+ grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr));
859
+ /* Columns need a real gutter, not a hairline. At 4px the right-hand value
860
+ of one column sat almost against the left-hand label of the next, and
861
+ the eye could not tell which label a value belonged to. */
862
+ column-gap: 28px;
863
+ }
864
+
865
+ /* At most two columns, however wide the window.
866
+
867
+ `max(24rem, 45%)` does both jobs in one expression: the percentage means
868
+ only two tracks can ever fit, and the absolute minimum means a narrow panel
869
+ drops to one rather than to two unreadably thin ones. Three columns looked
870
+ efficient on a wide screen and was not — a four-field table spread its
871
+ fields across the full width, so reading one row meant crossing the whole
872
+ monitor to pair a label with its value. */
873
+ .fields { grid-template-columns: repeat(auto-fill, minmax(max(21rem, 45%), 1fr)); }
874
+ .field {
875
+ display: grid; grid-template-columns: minmax(8rem, 38%) 1fr; gap: 16px;
876
+ padding: 6px 14px; align-items: baseline;
877
+ border-left: 2px solid transparent;
878
+ min-width: 0;
879
+ }
880
+ /* Anything that needs room takes the full width rather than being squeezed
881
+ into a column and wrapping to six lines. */
882
+ .field:has(.long-value) { grid-column: 1 / -1; }
883
+ .field:hover { background: var(--surface-2); border-left-color: var(--border-strong); }
884
+ .field-ref:hover { border-left-color: var(--accent); }
885
+ /* Three levels, and they have to be distinguishable at a glance:
886
+
887
+ value 13px, --text what you came to read
888
+ label 12px, --text-dim what it is called
889
+ type 10px, --text-faint what it is
890
+
891
+ Before this the label was 12px and the value 12.5px in the same colour, so
892
+ a field read as one undifferentiated string and the eye had to parse it.
893
+ Half a pixel is not a hierarchy. */
894
+ /* Labels wrap rather than truncate. `work_order_id integer` clipped to
895
+ `work_order_id integer…` hides the one thing the annotation was there to
896
+ say, and a column name is not something the reader can guess the rest of. */
897
+ .field dt {
898
+ color: var(--text-dim); font: 12px/1.5 var(--mono);
899
+ letter-spacing: -0.005em;
900
+ min-width: 0; overflow-wrap: anywhere;
901
+ }
902
+ .field dd {
903
+ margin: 0; font: 13px/1.5 var(--mono); color: var(--text);
904
+ word-break: break-word;
905
+ }
906
+ /* Prose escapes the monospace. Names read better in the interface face, and
907
+ nothing is lost — there is no column of them to line up. */
908
+ .field dd .prose { font-family: var(--sans); font-size: 13.5px; letter-spacing: -0.003em; }
909
+ .long-value.prose { font-family: var(--sans); font-size: 13px; }
910
+ .field dd.null { color: var(--text-faint); font-style: italic; }
911
+ .field .type { color: var(--text-faint); font-size: 10px; margin-left: 7px; }
912
+ .field .pk {
913
+ color: var(--key); font-size: 9.5px; margin-left: 5px;
914
+ text-transform: uppercase; letter-spacing: 0.05em;
915
+ }
916
+ .field .req {
917
+ color: var(--text-faint); font-size: 9.5px; margin-left: 5px;
918
+ text-transform: uppercase; letter-spacing: 0.05em;
919
+ }
920
+
921
+ /* Values render as what they are, matching the grid. Two views of the same
922
+ value should not disagree about its type. */
923
+ .bool-value.yes { color: var(--accent); }
924
+ .bool-value.no { color: var(--text-dim); }
925
+ .num-value { font-variant-numeric: tabular-nums; }
926
+ .date-value { color: var(--text); }
927
+ .long-value {
928
+ white-space: pre-wrap; line-height: 1.55;
929
+ background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
930
+ padding: 8px 10px; max-height: 14rem; overflow-y: auto;
931
+ }
932
+
933
+ /* A foreign key reads as one control: the value it holds, and where it goes. */
934
+ .ref-jump {
935
+ display: inline-flex; align-items: baseline; gap: 8px;
936
+ background: var(--bg); border: 1px solid var(--border); border-radius: 999px;
937
+ padding: 2px 4px 2px 10px; cursor: pointer;
938
+ font: 12.5px var(--mono); color: var(--text);
939
+ }
940
+ .ref-jump:hover { border-color: var(--accent); background: var(--accent-soft); }
941
+ .ref-jump-value { font-variant-numeric: tabular-nums; }
942
+ .ref-jump-table {
943
+ color: var(--text-dim); font-size: 11px;
944
+ background: var(--surface-2); border-radius: 999px; padding: 1px 8px;
945
+ }
946
+ .ref-jump:hover .ref-jump-table { color: var(--accent); }
947
+
948
+ /* The walk, unfolded in place: a caret beside a reference pins the row it
949
+ points at as an indented card, whose own references carry carets in turn. */
950
+ .nest-wrap { display: inline; }
951
+ .nest-caret {
952
+ border: none; background: none; cursor: pointer; padding: 2px 5px;
953
+ color: var(--text-faint); font-size: 11px; border-radius: 4px;
954
+ }
955
+ .nest-caret:hover { color: var(--accent); background: var(--accent-soft); }
956
+ .nest-card {
957
+ margin: 6px 0 2px; padding: 8px 12px;
958
+ border-left: 2px solid var(--border-strong); border-radius: 0 6px 6px 0;
959
+ background: var(--surface-2);
960
+ font-size: 12.5px;
961
+ }
962
+ .nest-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
963
+ .nest-table { font: 600 12px var(--mono); }
964
+ .nest-label { color: var(--text-dim); font-family: var(--mono); font-size: 12px; }
965
+ .nest-open { margin-left: auto; font-size: 11px; padding: 1px 7px; }
966
+ .nest-line { display: grid; grid-template-columns: minmax(8rem, 12rem) 1fr; gap: 10px; padding: 1px 0; }
967
+ .nest-name { color: var(--text-faint); font: 11.5px var(--mono); }
968
+ .nest-value { font-family: var(--mono); font-size: 12px; overflow-wrap: anywhere; }
969
+ .nest-value.null { color: var(--text-faint); font-style: italic; }
970
+ .nest-missing, .nest-more { margin: 4px 0 0; color: var(--text-faint); font-size: 11.5px; }
971
+
972
+ @media (prefers-reduced-motion: no-preference) {
973
+ .ref-jump, .field { transition: border-color 120ms ease, background 120ms ease; }
974
+ }
975
+
976
+ /* The Referenced-by panel is gone: the relationship tabs on the record carry
977
+ its counts, its navigation and the rows themselves, so keeping it meant
978
+ saying the same thing twice in two places. Its one unique job — showing a
979
+ relationship that has no rows — moved to a disabled tab. */
980
+
981
+
982
+ .loading { padding: 10px 12px; color: var(--text-dim); font-size: 12px; }
983
+
984
+ /* ---------- placeholders ---------- */
985
+
986
+ /* One bar, one sweep.
987
+
988
+ The animation is a single moving highlight rather than a pulse of the whole
989
+ block: a pulse makes a page of placeholders throb in unison, which draws
990
+ more attention than the content it is standing in for. Every bar shares one
991
+ timeline, so the sweep reads as one surface being revealed rather than
992
+ forty independent flickers. */
993
+ .sk-bar {
994
+ display: block;
995
+ border-radius: 3px;
996
+ background: var(--surface-2);
997
+ background-image: linear-gradient(
998
+ 90deg,
999
+ transparent 0%,
1000
+ color-mix(in srgb, var(--text-faint) 12%, transparent) 50%,
1001
+ transparent 100%
1002
+ );
1003
+ background-size: 240px 100%;
1004
+ background-repeat: no-repeat;
1005
+ animation: sk-sweep 1.25s linear infinite;
1006
+ }
1007
+
1008
+ @keyframes sk-sweep {
1009
+ from { background-position: -240px 0; }
1010
+ to { background-position: calc(100% + 240px) 0; }
1011
+ }
1012
+
1013
+ /* Motion is the decoration here, not the information — the shapes already say
1014
+ what is coming, so removing the sweep costs nothing. */
1015
+ @media (prefers-reduced-motion: reduce) {
1016
+ .sk-bar { animation: none; background-image: none; }
1017
+ }
1018
+
1019
+ .sk-grid { padding: 4px 12px 12px; }
1020
+ .sk-row {
1021
+ display: grid;
1022
+ grid-template-columns: repeat(var(--sk-cols), minmax(0, 1fr));
1023
+ gap: 16px; align-items: center;
1024
+ padding: 9px 2px;
1025
+ border-bottom: 1px solid var(--border);
1026
+ }
1027
+ .sk-head { border-bottom-color: var(--border-strong); }
1028
+ /* Placeholder rows sit at the same rhythm as real ones, so the grid does not
1029
+ change height when the data arrives. */
1030
+ .sk-row .sk-bar { justify-self: stretch; }
1031
+
1032
+ .sk-record { padding: 4px 22px 22px; }
1033
+ .sk-section { padding: 14px 0 4px; }
1034
+ .sk-section > .sk-bar { margin-bottom: 12px; }
1035
+ .sk-fields {
1036
+ display: grid;
1037
+ grid-template-columns: repeat(auto-fit, minmax(max(21rem, 45%), 1fr));
1038
+ gap: 2px 40px;
1039
+ }
1040
+ .sk-field {
1041
+ display: grid; grid-template-columns: 11rem minmax(0, 1fr);
1042
+ align-items: center; gap: 12px;
1043
+ padding: 9px 0;
1044
+ }
1045
+
1046
+ .sk-related { padding: 6px 0; }
1047
+ .sk-related-item {
1048
+ display: grid; grid-template-columns: minmax(2.4rem, auto) 1fr;
1049
+ align-items: center; gap: 12px;
1050
+ padding: 9px 14px;
1051
+ }
1052
+ .sk-related-names { display: flex; flex-direction: column; gap: 5px; }
1053
+ .note { padding: 10px 12px; color: var(--text-dim); font-size: 12px; margin: 0; }
1054
+
1055
+ /* Deliberately no transition on table rows. A hover you trigger continuously
1056
+ while reading should be instant; a fade makes the pointer feel like it is
1057
+ dragging the highlight behind it. */
1058
+
1059
+ /* ---------- write mode ----------
1060
+
1061
+ The indicator has to be visible without being looked for, so it rings the
1062
+ whole window rather than sitting quietly in a corner. */
1063
+
1064
+ .topbar-actions { display: flex; gap: 6px; flex: none; }
1065
+
1066
+ /* An icon's footprint, a mode's weight.
1067
+
1068
+ Read only is the resting state and gets no chrome at all; write mode gets a
1069
+ ring and a tint in the danger colour. The difference between them has to be
1070
+ readable at a glance from the corner of the eye — this is the control that
1071
+ says whether a keystroke can change the database. */
1072
+ button.mode {
1073
+ padding: 5px 9px; border-radius: 6px; line-height: 1;
1074
+ background: transparent; color: var(--text-dim);
1075
+ border: 1px solid transparent;
1076
+ font-size: 13px; cursor: pointer;
1077
+ }
1078
+ button.mode:hover { border-color: var(--border); background: var(--surface); }
1079
+ button.mode[aria-pressed="true"] {
1080
+ background: color-mix(in srgb, var(--danger) 14%, transparent);
1081
+ border-color: var(--danger);
1082
+ }
1083
+
1084
+ body.write-mode { box-shadow: inset 0 0 0 3px var(--danger); }
1085
+ body.write-mode .topbar { border-bottom-color: var(--danger); }
1086
+
1087
+ button.danger { background: var(--danger); color: var(--surface); border-color: var(--danger); }
1088
+
1089
+ /* ---------- dialogs ---------- */
1090
+
1091
+ dialog.confirm {
1092
+ border: 1px solid var(--border-strong); border-radius: 10px;
1093
+ background: var(--surface); color: var(--text);
1094
+ max-width: 34rem; padding: 20px 22px;
1095
+ font: 14px/1.55 var(--sans);
1096
+ }
1097
+ dialog.confirm::backdrop { background: rgb(0 0 0 / 0.45); }
1098
+ @media (prefers-reduced-motion: no-preference) {
1099
+ dialog.confirm {
1100
+ /* A modal appears in the middle of the viewport rather than out of a
1101
+ trigger, so it is the one popover that keeps a centre origin. */
1102
+ transition: opacity var(--t-panel) var(--ease-out), transform var(--t-panel) var(--ease-out);
1103
+ @starting-style { opacity: 0; transform: scale(0.97); }
1104
+ }
1105
+ dialog.confirm::backdrop {
1106
+ transition: background var(--t-panel) var(--ease-out);
1107
+ @starting-style { background: rgb(0 0 0 / 0); }
1108
+ }
1109
+ }
1110
+ dialog.confirm h2 { margin: 0 0 8px; font-size: 16px; letter-spacing: -0.01em; }
1111
+ dialog.confirm p { margin: 0 0 10px; color: var(--text-dim); }
1112
+ dialog.confirm strong { color: var(--text); font-family: var(--mono); font-size: 13px; }
1113
+ .confirm-terms { margin: 0 0 14px; padding-left: 18px; color: var(--text-dim); font-size: 13px; }
1114
+ .confirm-terms li { margin-bottom: 3px; }
1115
+ .confirm-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
1116
+ .confirm-diff { width: 100%; margin-bottom: 12px; font: 12.5px var(--mono); }
1117
+ .confirm-diff th { text-align: left; color: var(--text-dim); font-weight: 500; width: 5rem; text-transform: none; letter-spacing: 0; font-size: 12px; position: static; background: none; border: none; }
1118
+ .confirm-diff td { border: none; }
1119
+ .confirm-diff td.null { color: var(--text-faint); font-style: italic; }
1120
+ .confirm-params { font: 11.5px var(--mono); color: var(--text-faint); margin: 6px 0 0; }
1121
+ dialog.confirm .ddl { margin: 0; }
1122
+
1123
+ /* ---------- cell editor ---------- */
1124
+
1125
+ .cell-edit {
1126
+ width: 100%; padding: 3px 6px;
1127
+ font: 12.5px var(--mono); color: var(--text);
1128
+ background: var(--bg);
1129
+ border: 1px solid var(--border-strong); border-radius: 4px;
1130
+ }
1131
+ .cell-edit:focus { outline: 2px solid var(--danger); outline-offset: -1px; border-color: transparent; }
1132
+ .cell-edit[data-null="true"] { font-style: italic; }
1133
+
1134
+ /* ---------- toast ---------- */
1135
+
1136
+ .toast {
1137
+ padding: 8px 14px; flex: none;
1138
+ font-size: 13px;
1139
+ border-bottom: 1px solid var(--border);
1140
+ background: var(--surface-2); color: var(--text-dim);
1141
+ }
1142
+ @media (prefers-reduced-motion: no-preference) {
1143
+ .toast {
1144
+ /* A transition rather than a keyframe: transitions retarget mid-flight,
1145
+ keyframes restart from zero, and a second toast arriving during the
1146
+ first would otherwise jump. translateY in percent so the distance is
1147
+ the element's own height whatever it contains. */
1148
+ transition: opacity var(--t-panel) var(--ease-out), transform var(--t-panel) var(--ease-out);
1149
+ @starting-style { opacity: 0; transform: translateY(-100%); }
1150
+ }
1151
+ }
1152
+ .toast-ok { background: color-mix(in srgb, var(--accent) 12%, var(--surface)); color: var(--accent); }
1153
+ .toast-error { background: color-mix(in srgb, var(--danger) 12%, var(--surface)); color: var(--danger); }
1154
+
1155
+ /* ---------- schema diagram ---------- */
1156
+
1157
+ /* ---------- landing ---------- */
1158
+
1159
+ /* Kept for the query view's own saved-filter bar, which is not this card. */
1160
+ .landing-saved { margin-bottom: 26px; }
1161
+ .landing-heading {
1162
+ margin: 0 0 10px;
1163
+ font: 600 10.5px var(--sans); text-transform: uppercase; letter-spacing: 0.07em;
1164
+ color: var(--text-dim);
1165
+ }
1166
+ .landing-chips { display: flex; flex-wrap: wrap; gap: 6px; }
1167
+ /* One line, not two: the name and what makes it worth starting from read
1168
+ as a sentence, and stacking them doubled the height of the one block that
1169
+ pushed this card past its cap. */
1170
+ .landing-chip {
1171
+ display: flex; align-items: baseline; gap: 8px;
1172
+ background: var(--surface); border: 1px solid var(--border);
1173
+ border-radius: 3px; padding: 5px 10px; cursor: pointer; text-align: left;
1174
+ }
1175
+ .landing-chip:hover { border-color: var(--accent); background: var(--accent-soft); }
1176
+ .landing-chip-name { font: 12.5px var(--sans); color: var(--text); }
1177
+ .landing-chip-table { font: 10.5px var(--mono); color: var(--text-faint); }
1178
+ .landing-chip:hover .landing-chip-name { color: var(--accent); }
1179
+
1180
+ /* Where to start: the tables this database is actually built around, with
1181
+ their sizes. Clicking one runs it, which is also the first syntax lesson. */
1182
+ .landing-starts { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); }
1183
+ .landing-starts-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 10px; }
1184
+ .landing-starts-head .shape-label { margin: 0; }
1185
+ .start-chip .landing-chip-table { display: flex; gap: 6px; align-items: baseline; }
1186
+ .start-rows { color: var(--text-dim); }
1187
+
1188
+ /* ---------- find a value ---------- */
1189
+
1190
+ .find-value .conn-inner { max-width: 44rem; }
1191
+ .find-note { margin: 0 0 12px; font: 11.5px var(--sans); color: var(--text-faint); max-width: 46ch; }
1192
+ .find-input {
1193
+ width: 100%; padding: 8px 12px;
1194
+ font: 13px var(--mono); color: var(--text);
1195
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
1196
+ }
1197
+ .find-input:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
1198
+ .find-results { margin-top: 14px; max-height: 26rem; overflow-y: auto; }
1199
+ .find-group + .find-group { margin-top: 14px; }
1200
+ .find-group-head {
1201
+ display: flex; align-items: baseline; gap: 8px; margin: 0 0 6px;
1202
+ font: 600 10px var(--mono); text-transform: uppercase; letter-spacing: 0.07em;
1203
+ color: var(--text-dim);
1204
+ }
1205
+ .find-count { color: var(--text-faint); }
1206
+ .find-hit {
1207
+ display: flex; align-items: baseline; gap: 10px; width: 100%;
1208
+ padding: 5px 10px; text-align: left; cursor: pointer;
1209
+ background: none; border: 1px solid transparent; border-radius: var(--radius);
1210
+ }
1211
+ .find-hit:hover:not(:disabled) { background: var(--accent-soft); border-color: var(--border); }
1212
+ .find-hit:disabled { cursor: default; opacity: 0.7; }
1213
+ .find-hit-columns { flex: 0 0 9rem; font: 11px var(--mono); color: var(--text-faint); }
1214
+ .find-hit-value { font: 12.5px var(--mono); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1215
+ /* What the sweep could not do, in the colour of a caveat rather than a result. */
1216
+ .find-warn { color: var(--warn); }
1217
+
1218
+ /* ---------- check the shape ---------- */
1219
+
1220
+ .shape-check .conn-inner { max-width: 46rem; }
1221
+ .shape-check-group + .shape-check-group { margin-top: 16px; }
1222
+ .shape-check-table {
1223
+ display: flex; align-items: baseline; gap: 8px;
1224
+ padding: 3px 6px; margin: 0 0 6px -6px; cursor: pointer;
1225
+ background: none; border: 1px solid transparent; border-radius: var(--radius);
1226
+ font: 600 11px var(--mono); text-transform: uppercase; letter-spacing: 0.06em;
1227
+ color: var(--text);
1228
+ }
1229
+ .shape-check-table:hover { border-color: var(--border); background: var(--surface-2); color: var(--accent); }
1230
+ .shape-check-count { color: var(--text-faint); font-weight: 400; }
1231
+ .shape-check-rule-group {
1232
+ margin: 0 0 10px; padding-left: 10px;
1233
+ border-left: 2px solid var(--border);
1234
+ }
1235
+ .shape-check-rule-group.tone-high { border-left-color: var(--danger); }
1236
+ .shape-check-rule-group.tone-medium { border-left-color: var(--warn); }
1237
+ /* The cost, once per rule; then the columns it applies to, indented under
1238
+ it in the tool's own voice — a column and an arrow, nothing else. */
1239
+ .shape-check-why {
1240
+ display: flex; gap: 8px; align-items: baseline; margin: 0 0 3px;
1241
+ font: 12px var(--sans); color: var(--text-dim);
1242
+ }
1243
+ .shape-check-rule { flex: 0 0 10.5rem; font: 11px var(--mono); color: var(--text-faint); }
1244
+ .shape-check-item {
1245
+ margin: 0 0 2px 10.5rem; padding-left: 8px;
1246
+ font: 12px var(--mono); color: var(--text);
1247
+ }
1248
+
1249
+ /* ---------- pages, on the home screen ---------- */
1250
+
1251
+ /* Two columns, and one when there is only one card to show.
1252
+
1253
+ `:only-child` is the whole first-run rule: a database nobody has worked in
1254
+ yet has no work to resume, `workPanel` returns nothing rather than an empty
1255
+ card, and the schema takes the full width without anything having to decide
1256
+ that it should. */
1257
+ /* The work column takes the width it needs, up to a share of the page; the
1258
+ schema takes the rest. Equal halves stranded a column on a fresh browser,
1259
+ where "your work" is one short list and the summary beside it is six lines
1260
+ of prose — half a screen of nothing next to something that wanted the
1261
+ room. */
1262
+ /* `stretch`, not `start`: two cards of different heights side by side read
1263
+ as one card and one offcut. Equal height costs the shorter one some space
1264
+ at the bottom, which is what a column of a row looks like. */
1265
+ .landing-top {
1266
+ display: grid; gap: 16px; align-items: stretch;
1267
+ /* The database on the left, where reading starts and where the answer for
1268
+ everyone is; your work beside it, in a bounded column — every line in
1269
+ that card wraps, so `fit-content` resolves to min-content and collapses
1270
+ it to a sliver, while an equal half strands a screen of nothing. */
1271
+ grid-template-columns: minmax(0, 1fr) minmax(17rem, 27rem);
1272
+ margin-bottom: 16px;
1273
+ }
1274
+ .landing-top > :only-child { grid-column: 1 / -1; }
1275
+ .landing-top > .shape { max-width: none; margin: 0; }
1276
+ /* Under a third of the window, whatever is in them.
1277
+
1278
+ Both cards grow with their content — a wide schema has more to say, and
1279
+ "your work" gains a line every time you open a record — so a row that
1280
+ sizes to its content is a row that eventually owns the screen. The cap is
1281
+ on the row, and each card scrolls its own overflow: the diagram below is
1282
+ what the page is for, and it should never be pushed off by a list of
1283
+ things you did yesterday.
1284
+
1285
+ 30vh rather than a quarter: a quarter cut the schema summary's own lines
1286
+ on a laptop, and the summary is the part a first-time reader needs whole. */
1287
+ .landing-top > .shape {
1288
+ max-height: 30vh;
1289
+ overflow-y: auto;
1290
+ /* A scroll that reaches the end stops there rather than carrying on down
1291
+ the page, which is what makes a scrolling panel usable inside one. */
1292
+ overscroll-behavior: contain;
1293
+ /* A shadow at whichever edge has more behind it.
1294
+
1295
+ A capped panel clips, and a clipped row reads as broken rather than as
1296
+ "there is more" — the cut lands mid-chip and looks like a rendering
1297
+ fault. The two `local` gradients scroll with the content and cover the
1298
+ shadows when the content is at that end; the two fixed ones show
1299
+ through when it is not. No script, and nothing to show when the card
1300
+ does not scroll at all, which is the case this has to get right. */
1301
+ background:
1302
+ linear-gradient(var(--surface) 30%, rgb(0 0 0 / 0)) top / 100% 14px no-repeat local,
1303
+ linear-gradient(rgb(0 0 0 / 0), var(--surface) 70%) bottom / 100% 14px no-repeat local,
1304
+ radial-gradient(farthest-side at 50% 0, rgb(0 0 0 / 0.09), rgb(0 0 0 / 0)) top / 100% 6px no-repeat,
1305
+ radial-gradient(farthest-side at 50% 100%, rgb(0 0 0 / 0.09), rgb(0 0 0 / 0)) bottom / 100% 6px no-repeat,
1306
+ var(--surface);
1307
+ }
1308
+ /* Each card answers to its own width, not the window's.
1309
+
1310
+ The label gutter is 9.5rem, which is a quarter of a column and a tenth of
1311
+ a page — so the same rule that reads well full-width squeezes the content
1312
+ into a ribbon beside it. A container query lets the card stack its labels
1313
+ when *it* is narrow, whatever the window is doing. */
1314
+ .landing-top > .shape { container-type: inline-size; }
1315
+ /* Scoped through `.landing-top` so these beat the base rules whatever order
1316
+ the file happens to put them in — the plain `.shape-label` is defined a
1317
+ thousand lines further down, and would otherwise win on cascade order. */
1318
+ @container (max-width: 34rem) {
1319
+ .landing-top .shape-line { grid-template-columns: minmax(0, 1fr); gap: 2px; }
1320
+ .landing-top .shape-label { text-align: left; }
1321
+ }
1322
+ /* Two columns of a schema summary and a work list stop being readable well
1323
+ before the window does — the label column alone is 9.5rem. */
1324
+ @media (max-width: 1180px) { .landing-top { grid-template-columns: minmax(0, 1fr); } }
1325
+
1326
+ /* The same card as the schema summary beside it — including no margin of its
1327
+ own. It carried `margin-bottom: 16px` from when it was a full-width band
1328
+ below the schema, and that rule sits after the `.landing-top > .shape`
1329
+ reset at equal specificity, so it won: the row stretched both cards to the
1330
+ same box, then this card gave 16px of its box back and ended short. The
1331
+ gap and the row's own margin do the spacing now. */
1332
+ .landing-pages .landing-heading { margin-bottom: 12px; }
1333
+
1334
+ /* Links on a line, not cards in a grid. Wrapping, so a long list of recent
1335
+ records fills the width it has rather than forcing a scroll. */
1336
+ .landing-links { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: baseline; }
1337
+ .landing-link {
1338
+ display: inline-flex; align-items: baseline; gap: 6px;
1339
+ padding: 1px 4px; margin: 0 -4px; cursor: pointer;
1340
+ background: none; border: none; border-radius: 2px;
1341
+ font: 12.5px var(--mono); color: var(--link);
1342
+ text-align: left;
1343
+ }
1344
+ .landing-link:hover { background: var(--accent-soft); color: var(--accent); }
1345
+ .landing-link:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
1346
+ .landing-link-detail { color: var(--text-faint); font-size: 11px; }
1347
+ .landing-all .landing-page-filter { margin-bottom: 6px; }
1348
+ .landing-group + .landing-group { margin-top: 16px; }
1349
+ .landing-sub {
1350
+ margin: 0 0 8px;
1351
+ font: 600 10px var(--mono); text-transform: uppercase; letter-spacing: 0.07em;
1352
+ color: var(--text-faint);
1353
+ }
1354
+ .landing-sub-row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
1355
+ .landing-sub-row .landing-sub { margin-bottom: 8px; }
1356
+ .landing-page-filter {
1357
+ flex: 0 1 14rem; min-width: 8rem;
1358
+ padding: 4px 10px; font: 11.5px var(--mono);
1359
+ background: var(--surface); color: var(--text);
1360
+ border: 1px solid var(--border); border-radius: var(--radius);
1361
+ }
1362
+ .landing-page-filter:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
1363
+
1364
+ /* Scrollable rather than capped: forty pages is where finding one matters
1365
+ more than seeing all of them, and a "+31 more" is a dead end. */
1366
+ /* Rows, not a panel: a grey block inside a card is a second card, and the
1367
+ list is short enough that its own border adds nothing but an edge. No
1368
+ height of its own either — the card is the scroller now, and a scroller
1369
+ inside a scroller is two things to fight with. */
1370
+ .landing-page-list { max-width: 38rem; }
1371
+ .landing-page {
1372
+ display: flex; align-items: baseline; gap: 10px; width: 100%;
1373
+ padding: 6px; margin-left: -6px; text-align: left; cursor: pointer;
1374
+ background: none; border: none; border-bottom: 1px solid var(--border);
1375
+ }
1376
+ .landing-page:last-child { border-bottom: none; }
1377
+ .landing-page:hover { background: var(--accent-soft); }
1378
+ .landing-page:hover .landing-page-name { color: var(--accent); }
1379
+ .landing-page-name { flex: 1 1 auto; font: 12.5px var(--sans); color: var(--text); }
1380
+ .landing-page-base { font: 11px var(--mono); color: var(--text-dim); }
1381
+ .landing-page-source { font: 10px var(--mono); color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.05em; }
1382
+ .landing-page-list .note { margin: 0; padding: 8px 12px; }
1383
+
1384
+ /* The star beside a page's name. Quiet until it means something. */
1385
+ .page-pin {
1386
+ padding: 2px 6px; font-size: 14px; line-height: 1;
1387
+ color: var(--text-faint); border-color: transparent; background: none;
1388
+ }
1389
+ .page-pin:hover { color: var(--text); border-color: var(--border); }
1390
+ .page-pin.is-pinned { color: var(--warn); }
1391
+
1392
+ .diagram-title { margin: 0 0 14px; font: 600 16px var(--mono); letter-spacing: -0.01em; }
1393
+
1394
+ .node-view rect { stroke-dasharray: 4 3; }
1395
+ .node.dim { opacity: 0.22; }
1396
+ .node.lit rect { stroke: var(--accent); stroke-width: 2; fill: var(--accent-soft); }
1397
+ .node.lit text { fill: var(--accent); font-weight: 600; }
1398
+ .schema-edges path.dim { opacity: 0.1; }
1399
+ @media (prefers-reduced-motion: no-preference) {
1400
+ .node, .schema-edges path { transition: opacity 120ms ease; }
1401
+ }
1402
+
1403
+ /* With no referring tables there is no second panel, so the fields should
1404
+ use the width rather than leaving a column of nothing beside them. */
1405
+ .record-solo { grid-template-columns: minmax(0, 1fr); }
1406
+
1407
+ /* ---------- connection picker ---------- */
1408
+
1409
+ /* In the top bar now, so it sizes to its content rather than filling a
1410
+ sidebar column: a dropdown stretched across the header would read as a
1411
+ second query bar. Capped, because a connection named after a URL should
1412
+ not push the query bar off the screen. */
1413
+ .conn-picker { display: flex; gap: 6px; align-items: center; flex: none; }
1414
+ .conn-picker .dd { min-width: 0; max-width: 200px; }
1415
+ .conn-picker[hidden] { display: none; }
1416
+ .conn-label {
1417
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em;
1418
+ color: var(--text-faint); flex: none;
1419
+ }
1420
+
1421
+ .field-stack { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
1422
+ .field-stack span { font-size: 12px; color: var(--text-dim); }
1423
+
1424
+ /* ---------- saved filters ---------- */
1425
+
1426
+ .views {
1427
+ display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
1428
+ padding: 8px 14px 0;
1429
+ }
1430
+ /* Saved filters on the left, what the query currently says on the right, with
1431
+ room between them: one is a thing you chose to keep, the other is the state
1432
+ of this moment, and running them together reads as one list of eight things. */
1433
+ .views .clause:first-of-type { margin-left: 10px; }
1434
+ .view-tab-wrap {
1435
+ display: inline-flex; align-items: stretch;
1436
+ border: 1px solid var(--border); border-radius: 999px;
1437
+ overflow: hidden;
1438
+ }
1439
+ .view-tab-wrap.active { border-color: var(--accent); background: var(--accent-soft); }
1440
+ .view-tab {
1441
+ background: none; border: none; border-radius: 0;
1442
+ color: var(--text-dim); padding: 3px 10px;
1443
+ font: 12px var(--sans); cursor: pointer;
1444
+ }
1445
+ .views > .view-tab {
1446
+ border: 1px solid var(--border); border-radius: 999px;
1447
+ }
1448
+ .view-tab[aria-selected="true"] { color: var(--accent); font-weight: 600; }
1449
+ .view-tab:hover { color: var(--text); }
1450
+ .view-remove {
1451
+ background: none; border: none; border-radius: 0;
1452
+ color: var(--text-faint); cursor: pointer;
1453
+ padding: 3px 7px 3px 2px; font-size: 13px; line-height: 1;
1454
+ }
1455
+ .view-remove:hover { color: var(--danger); }
1456
+ .view-save {
1457
+ background: none; border: 1px dashed var(--border-strong); border-radius: 999px;
1458
+ color: var(--text-dim); padding: 3px 10px; font: 12px var(--sans); cursor: pointer;
1459
+ }
1460
+ .view-save:hover { border-color: var(--accent); color: var(--accent); border-style: solid; }
1461
+
1462
+ /* What the query currently says, one chip per clause.
1463
+
1464
+ These had no rules at all: the label and the value ran together as
1465
+ "showingname, country_code", and the remove button fell back to the app's
1466
+ default button — a filled accent square twice the height of the chip it
1467
+ was supposed to sit inside.
1468
+
1469
+ Deliberately quieter than a saved view beside it. A saved filter is
1470
+ something you chose to keep; a clause chip is the state of this moment,
1471
+ and it should read as a description of the result with a way off rather
1472
+ than as a control competing with the tabs. */
1473
+ .clause {
1474
+ display: inline-flex; align-items: baseline; gap: 5px;
1475
+ padding: 2px 4px 2px 9px;
1476
+ border: 1px solid var(--border); border-radius: 999px;
1477
+ background: var(--surface-2);
1478
+ max-width: 22rem;
1479
+ }
1480
+ .clause-label {
1481
+ font: 10px var(--mono); text-transform: uppercase; letter-spacing: 0.05em;
1482
+ color: var(--text-faint); flex: none;
1483
+ }
1484
+ /* The value is the part worth reading, so it gets the room and the ellipsis:
1485
+ a `where` chip on a long `in (...)` list must not push the rest off the
1486
+ row. The full text is on the title. */
1487
+ .clause-text {
1488
+ font: 11.5px var(--mono); color: var(--text-dim);
1489
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1490
+ }
1491
+ .clause-remove {
1492
+ flex: none;
1493
+ background: none; border: none;
1494
+ padding: 0 5px; border-radius: 999px;
1495
+ font-size: 13px; line-height: 1; cursor: pointer;
1496
+ color: var(--text-faint);
1497
+ }
1498
+ .clause-remove:hover { color: var(--danger); }
1499
+ .clause-remove:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
1500
+
1501
+ /* ---------- themed listbox ---------- */
1502
+
1503
+ .dd { position: relative; }
1504
+
1505
+ .dd-button {
1506
+ width: 100%;
1507
+ display: flex; align-items: center; justify-content: space-between; gap: 6px;
1508
+ padding: 5px 8px;
1509
+ font: 12px var(--mono); color: var(--text);
1510
+ background: var(--bg);
1511
+ border: 1px solid var(--border); border-radius: var(--radius);
1512
+ cursor: pointer;
1513
+ }
1514
+ .dd-button:hover { border-color: var(--border-strong); }
1515
+ .dd-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1516
+ .dd-caret { color: var(--text-faint); font-size: 10px; flex: none; }
1517
+
1518
+ .dd-list {
1519
+ /* Positioned in the viewport by dropdown.js on open; these are the
1520
+ fallback values for the moment before that runs. */
1521
+ position: fixed; z-index: 40; top: 0; left: 0;
1522
+ min-width: 100%; max-width: 22rem; max-height: 18rem; overflow-y: auto;
1523
+ background: var(--surface);
1524
+ border: 1px solid var(--border-strong); border-radius: 8px;
1525
+ /* The one place a shadow earns its keep: a popover has to read as floating
1526
+ above the page rather than punched into it. */
1527
+ box-shadow: 0 6px 20px rgb(0 0 0 / 0.16);
1528
+ padding: 4px;
1529
+ }
1530
+ .dd-list[hidden] { display: none; }
1531
+ @media (prefers-reduced-motion: no-preference) {
1532
+ .dd-list {
1533
+ /* From the trigger, not the centre, so the menu reads as coming out of
1534
+ the thing that opened it. Never scale(0) — nothing in the real world
1535
+ appears from nothing. */
1536
+ transform-origin: top left;
1537
+ transition: opacity var(--t-popover) var(--ease-out), transform var(--t-popover) var(--ease-out);
1538
+ @starting-style { opacity: 0; transform: scale(0.96) translateY(-4px); }
1539
+ }
1540
+ }
1541
+
1542
+ .dd-option {
1543
+ display: flex; align-items: flex-start; gap: 6px;
1544
+ padding: 5px 7px; border-radius: 5px;
1545
+ cursor: pointer;
1546
+ }
1547
+ .dd-option.active { background: var(--surface-2); }
1548
+ .dd-option.selected .dd-label { color: var(--accent); font-weight: 600; }
1549
+ .dd-option.disabled { opacity: 0.5; cursor: default; }
1550
+ .dd-check { width: 0.9em; flex: none; color: var(--accent); font-size: 11px; line-height: 1.5; }
1551
+ .dd-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
1552
+ .dd-label { font: 12.5px var(--mono); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1553
+ .dd-detail { font: 11px var(--mono); color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1554
+ .dd-badge {
1555
+ flex: none; align-self: center;
1556
+ font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.05em;
1557
+ color: var(--text-dim); background: var(--surface-2);
1558
+ border-radius: 3px; padding: 1px 5px;
1559
+ }
1560
+ .dd-option.tone-error .dd-label { color: var(--danger); }
1561
+ .dd-option.tone-error .dd-detail::after { content: " · unreachable"; color: var(--danger); }
1562
+ .dd-divider { height: 1px; background: var(--border); margin: 4px 2px; }
1563
+ .dd-action .dd-label { color: var(--text-dim); font-family: var(--sans); font-size: 12px; }
1564
+ .dd-action:hover .dd-label { color: var(--accent); }
1565
+
1566
+ /* ---------- pending edits ---------- */
1567
+
1568
+ .pending {
1569
+ display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
1570
+ padding: 8px 12px;
1571
+ background: color-mix(in srgb, var(--danger) 10%, var(--surface));
1572
+ border-bottom: 1px solid var(--border);
1573
+ }
1574
+ .pending-count { font-size: 12.5px; font-weight: 600; color: var(--danger); }
1575
+ .pending-cols {
1576
+ flex: 1; min-width: 0;
1577
+ font: 11.5px var(--mono); color: var(--text-dim);
1578
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1579
+ }
1580
+ .pending button { padding: 4px 10px; font-size: 12px; }
1581
+
1582
+ .cell-edit.dirty {
1583
+ border-color: var(--danger);
1584
+ background: color-mix(in srgb, var(--danger) 7%, var(--bg));
1585
+ }
1586
+
1587
+ .confirm-wide { max-width: 42rem; }
1588
+ .confirm-diff td { padding: 3px 8px 3px 0; vertical-align: baseline; }
1589
+ .diff-col { font: 12.5px var(--mono); color: var(--text); }
1590
+ .diff-from { color: var(--text-dim); text-decoration: line-through; }
1591
+ .diff-arrow { color: var(--text-faint); padding: 0 4px; }
1592
+ .diff-to { color: var(--text); font-weight: 600; }
1593
+ .confirm-diff .null { font-style: italic; text-decoration: none; color: var(--text-faint); }
1594
+ .diff-type { color: var(--text-faint); font: 10.5px var(--mono); margin-left: 7px; }
1595
+
1596
+ /* The table a row crumb belongs to, shown only when the walk changed table.
1597
+
1598
+ No punctuation between the table and the label. The trail already uses a
1599
+ chevron to separate crumbs, and a second separator inside a crumb competes
1600
+ with it — `work_order › WO-100002 › customer · Blackwood Joinery` reads as
1601
+ four things at one level rather than three, one of which is qualified.
1602
+ Size and colour do the binding instead: the table is smaller, lighter and
1603
+ sits on the baseline as a qualifier, the label carries the weight. */
1604
+ .crumb-table {
1605
+ /* --text-dim, not --text-faint: faint carries 50% alpha, which at this
1606
+ size disappears rather than recedes. The qualifier has to be readable —
1607
+ it is the answer to "what am I looking at" — it just should not compete
1608
+ with the label. Size and weight do that; opacity was doing too much. */
1609
+ color: var(--text-dim);
1610
+ font-size: 11.5px;
1611
+ margin-right: 5px;
1612
+ }
1613
+ .crumbs button:hover .crumb-table { color: var(--text); }
1614
+
1615
+ /* Two states can apply to the same row and they must not look alike: the
1616
+ table you are *on*, and the row Enter would open. Before this they were a
1617
+ soft tint and a slightly different soft tint, so arrowing through the list
1618
+ left you unable to tell which one was armed.
1619
+
1620
+ The keyboard highlight wins outright wherever both apply — it describes
1621
+ what is about to happen, which matters more than what already has. */
1622
+ /* One indicator, used for both "this is the table you are looking at" and
1623
+ "this is where the keyboard is". They are the same row almost all the time,
1624
+ and giving the current table a fainter version of the same treatment made
1625
+ the strong one read as a different kind of thing rather than as the same
1626
+ thing with focus. */
1627
+ #table-list button[aria-current="true"],
1628
+ #table-list button.active {
1629
+ background: var(--accent-soft);
1630
+ color: var(--accent);
1631
+ box-shadow: inset 3px 0 0 var(--accent);
1632
+ }
1633
+
1634
+ /* The highlight *is* the focus indicator here.
1635
+
1636
+ Now that arrowing moves real focus, the browser's ring was drawn on top of
1637
+ a row that already said it was the current one — two selection treatments
1638
+ on one row, one of them a heavy outlined rectangle. The lightweight
1639
+ indicator was doing the job; the ring only appears on a row that has focus
1640
+ without being the highlighted one, which cannot happen. */
1641
+ #table-list button:focus-visible {
1642
+ outline: none;
1643
+ background: var(--accent-soft);
1644
+ color: var(--accent);
1645
+ box-shadow: inset 3px 0 0 var(--accent);
1646
+ }
1647
+ /* While the cursor is away from the table on screen, the on-screen table's
1648
+ marker steps back to a whisper. One "you are here" at a time: during a
1649
+ quick run down the list the debounced preview lags the cursor, and both
1650
+ rows wearing the full treatment reads as two tables selected. */
1651
+ #table-list.skimming button[aria-current="true"]:not(.active) {
1652
+ background: none;
1653
+ color: inherit;
1654
+ box-shadow: inset 3px 0 0 var(--border-strong);
1655
+ }
1656
+
1657
+ /* Names what the highlight means, so the affordance does not have to be
1658
+ inferred from a colour. */
1659
+ /* Only on a row you have not opened yet. On the table already on screen,
1660
+ "press Enter" would be an instruction to do what has been done. */
1661
+ #table-list button.active:not([aria-current="true"])::after {
1662
+ content: "↵";
1663
+ margin-left: auto;
1664
+ padding-left: 8px;
1665
+ color: var(--accent);
1666
+ font-size: 11px;
1667
+ }
1668
+
1669
+ /* The new-row form: the schema as a column of labelled boxes. */
1670
+ .insert-dialog { max-width: 34rem; width: calc(100vw - 48px); }
1671
+ .insert-lede { margin: 4px 0 12px; font-size: 12.5px; color: var(--text-dim); }
1672
+ .insert-fields {
1673
+ display: grid; gap: 8px; max-height: 55vh; overflow-y: auto; padding-right: 4px;
1674
+ }
1675
+ .insert-field { display: grid; grid-template-columns: minmax(9rem, 14rem) 1fr; gap: 10px; align-items: center; }
1676
+ .insert-field label { font: 12.5px var(--mono); color: var(--text); overflow-wrap: anywhere; }
1677
+ .insert-field .type { margin-left: 6px; color: var(--text-faint); font-size: 10.5px; }
1678
+ .insert-input {
1679
+ padding: 5px 8px; font: 13px var(--mono);
1680
+ background: var(--surface); color: var(--text);
1681
+ border: 1px solid var(--border); border-radius: 6px; min-width: 0;
1682
+ }
1683
+ .insert-input:focus { outline: none; border-color: var(--accent); }
1684
+ .insert-problem { margin: 10px 0 0; color: var(--danger); font-size: 12.5px; }
1685
+ @media (max-width: 700px) {
1686
+ .insert-field { grid-template-columns: 1fr; gap: 2px; }
1687
+ }
1688
+
1689
+ /* A field that just landed. Green for a couple of seconds, then gone — a
1690
+ confirmation you have to dismiss is a confirmation you learn to ignore. */
1691
+ .cell-edit.saved {
1692
+ border-color: var(--accent);
1693
+ background: var(--accent-soft);
1694
+ }
1695
+ @media (prefers-reduced-motion: no-preference) {
1696
+ .cell-edit { transition: border-color 160ms ease, background 160ms ease; }
1697
+ }
1698
+
1699
+ /* ---------- query autocomplete ---------- */
1700
+
1701
+ .querybar { position: relative; }
1702
+ .ac {
1703
+ position: absolute; z-index: 30; top: calc(100% + 4px); left: 0;
1704
+ min-width: 22rem; max-width: 34rem; max-height: 19rem; overflow-y: auto;
1705
+ background: var(--surface);
1706
+ border: 1px solid var(--border-strong); border-radius: 8px;
1707
+ box-shadow: 0 6px 20px rgb(0 0 0 / 0.16);
1708
+ padding: 4px;
1709
+ }
1710
+ .ac[hidden] { display: none; }
1711
+ @media (prefers-reduced-motion: no-preference) {
1712
+ .ac {
1713
+ transform-origin: top left;
1714
+ transition: opacity var(--t-popover) var(--ease-out), transform var(--t-popover) var(--ease-out);
1715
+ @starting-style { opacity: 0; transform: scale(0.97) translateY(-4px); }
1716
+ }
1717
+ }
1718
+ .ac-item {
1719
+ display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
1720
+ padding: 4px 8px; border-radius: 5px; cursor: pointer;
1721
+ }
1722
+ .ac-item:hover, .ac-item.active { background: var(--surface-2); }
1723
+ /* A Shift-extended run, where the clause takes a list. Accented rather than
1724
+ grey so the range is distinguishable from the cursor sitting on one item —
1725
+ the two states look identical if both are just "shaded". */
1726
+ .ac-item.picked { background: var(--accent-soft); color: var(--accent); }
1727
+ .ac-item.picked .ac-detail { color: var(--accent); opacity: 0.7; }
1728
+ .ac-item.picked.active { box-shadow: inset 2px 0 0 var(--accent); }
1729
+ .ac-label { font: 12.5px var(--mono); color: var(--text); }
1730
+ .ac-detail { font: 11px var(--mono); color: var(--text-dim); flex: none; }
1731
+ /* Kind is carried by colour on the label, so the list scans by category
1732
+ without needing a column of icons. */
1733
+ .kind-table .ac-label { color: var(--accent); }
1734
+ .kind-op .ac-label { color: var(--key); }
1735
+ .kind-kw .ac-label { color: var(--warn); }
1736
+ .kind-value .ac-label { color: var(--link); }
1737
+
1738
+ /* Virtualised rows: the spacers are layout only and must never be styled as
1739
+ content, or the striping and hover follow them. */
1740
+ .virtual-spacer, .virtual-spacer:hover { background: none; cursor: default; }
1741
+ .virtual-note { color: var(--text-faint); font-size: 11.5px; padding: 8px 14px 0; }
1742
+
1743
+ /* ---------- children on the record page ----------
1744
+
1745
+ The Referenced-by panel is the index — counts, and a way to jump. These are
1746
+ the data. A record that says "5 parts" and makes you click to see them is
1747
+ answering a question nobody asked; reading them is usually why the record
1748
+ was opened. */
1749
+
1750
+ .record-children {
1751
+ grid-column: 1 / -1;
1752
+ border-top: 1px solid var(--border);
1753
+ }
1754
+ /* The relationship index, in its own column.
1755
+
1756
+ It spent a while as a horizontal strip under the record, which put the list
1757
+ of what a row connects to below a screenful of fields — so finding out what
1758
+ a record relates to meant scrolling past the record. A column keeps it in
1759
+ view, and a vertical list has room for a dozen relationships where a tab
1760
+ strip wraps to three rows. */
1761
+
1762
+ /* Stays in view while the record scrolls.
1763
+
1764
+ It is a navigator, not part of the record, and a navigator that scrolls
1765
+ away leaves you paging back up to change what you are looking at. Anchored
1766
+ to the top of the grid rather than stretched, so `position: sticky` has a
1767
+ box shorter than the scroller to stick within. */
1768
+ .related-panel {
1769
+ display: flex; flex-direction: column;
1770
+ position: sticky; top: 0; align-self: start;
1771
+ max-height: 100vh; overflow-y: auto;
1772
+ }
1773
+ .related-list { display: flex; flex-direction: column; padding: 6px 0; }
1774
+ /* The rail's own header, which is the panel heading plus one control.
1775
+
1776
+ `.panel > h3` styles a heading that *is* the header; here the heading is
1777
+ inside one, so the sticky, the ground and the rule move out to the wrapper
1778
+ and the h3 keeps only its type. Without that the header scrolls away and
1779
+ the list runs under the trail above it. */
1780
+ .related-head {
1781
+ display: flex; align-items: center; gap: 8px;
1782
+ padding: 4px 14px 4px;
1783
+ background: var(--surface);
1784
+ border-bottom: 1px solid var(--border);
1785
+ position: sticky; top: 0; z-index: 1;
1786
+ }
1787
+ .related-head > h3 {
1788
+ margin: 0; padding: 6px 0;
1789
+ font: 600 10.5px var(--sans); text-transform: uppercase; letter-spacing: 0.09em;
1790
+ color: var(--text-dim);
1791
+ }
1792
+ /* Quiet until wanted: the list is what the rail is for, and the control that
1793
+ shortens it should not compete with the first row of it. */
1794
+ .related-empties {
1795
+ margin-left: auto; flex: none;
1796
+ padding: 2px 7px; font: 10px var(--mono);
1797
+ color: var(--text-faint); border-color: transparent; background: none;
1798
+ }
1799
+ .related-empties:hover { color: var(--accent); border-color: var(--border); }
1800
+ .related-list.hide-empty .related-item.empty { display: none; }
1801
+
1802
+
1803
+
1804
+ /* Pushed to the far end of the trail, where it is out of the way of reading
1805
+ the trail itself but lands under the cursor after a click on the last
1806
+ crumb. */
1807
+ .crumbs .crumb-back {
1808
+ margin-left: auto;
1809
+ display: inline-flex; align-items: center; gap: 6px;
1810
+ padding: 3px 10px; border-radius: 5px;
1811
+ font: 11.5px var(--mono); color: var(--text-dim);
1812
+ background: none; border: 1px solid var(--border); cursor: pointer;
1813
+ }
1814
+ .crumbs .crumb-back:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
1815
+
1816
+ .related-item {
1817
+ display: grid; grid-template-columns: minmax(2.4rem, auto) 1fr;
1818
+ align-items: center; gap: 12px;
1819
+ /* A grid inside a <button> inherits the UA stylesheet's centring, which
1820
+ sizes each row to its own content and then centres it — so a row with a
1821
+ shorter table name sat further right than the one under it. Every row
1822
+ starts at the same edge whether it has rows behind it or not. */
1823
+ width: 100%; justify-content: start;
1824
+ background: none; border: none; border-radius: 0;
1825
+ border-left: 2px solid transparent;
1826
+ padding: 9px 14px; text-align: left; cursor: pointer;
1827
+ }
1828
+ .related-item:hover:not([disabled]) { background: var(--accent-soft); }
1829
+ .related-item[aria-selected="true"] {
1830
+ background: var(--accent-soft);
1831
+ border-left-color: var(--accent);
1832
+ }
1833
+ .related-item[disabled] { cursor: default; opacity: 0.55; }
1834
+
1835
+ .related-count {
1836
+ font: 600 16px var(--mono); color: var(--accent);
1837
+ font-variant-numeric: tabular-nums; text-align: right;
1838
+ letter-spacing: -0.02em;
1839
+ }
1840
+ .related-item.empty .related-count { color: var(--text-faint); font-weight: 400; }
1841
+ .related-names { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
1842
+ .related-table {
1843
+ font: 12.5px var(--mono); color: var(--text);
1844
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1845
+ }
1846
+ .related-item.empty .related-table { color: var(--text-dim); }
1847
+ .related-via {
1848
+ font: 10.5px var(--mono); color: var(--text-faint);
1849
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1850
+ }
1851
+
1852
+ /* Separated by space and its own heading rather than a rule. On a
1853
+ four-field record a border here was the third horizontal line in a page
1854
+ with almost nothing in it, which reads as fragments rather than a record. */
1855
+ .record-children { grid-column: 1 / -1; margin-top: 10px; }
1856
+
1857
+ /* Room to breathe at the end. Content ending flush against the bottom edge
1858
+ reads as cut off rather than finished, and on a scrolled page there is no
1859
+ signal that you have reached the end of it. */
1860
+ .record { padding-bottom: 64px; }
1861
+ .record .panel { padding-bottom: 12px; }
1862
+
1863
+ /* A record has a measure; a table does not.
1864
+
1865
+ On a wide window with the table list hidden, a four-field record was spread
1866
+ across two thousand pixels — the label at one edge and its value at the
1867
+ other, with the eye having to travel the whole way to pair them. Capped and
1868
+ centred, the pairs stay together. Tables are left alone: columns of data
1869
+ genuinely want every pixel, and a table is scanned down rather than read
1870
+ across. */
1871
+ .record {
1872
+ width: 100%;
1873
+ max-width: 1500px;
1874
+ margin-inline: auto;
1875
+ }
1876
+
1877
+ .record-child-head {
1878
+ display: flex; align-items: baseline; gap: 10px;
1879
+ padding: 12px 14px 6px;
1880
+ }
1881
+ .record-child-head h3 {
1882
+ margin: 0;
1883
+ font: 600 10.5px var(--sans); text-transform: uppercase; letter-spacing: 0.09em;
1884
+ color: var(--text-dim);
1885
+ }
1886
+ .record-child-via { font: 10.5px var(--mono); color: var(--text-faint); }
1887
+ .record-child-head button { margin-left: auto; padding: 3px 10px; font-size: 11.5px; }
1888
+
1889
+ /* Embedded grids sit tighter than the main one: they are a supporting detail
1890
+ on a page, not the thing being read. */
1891
+ /* Embedded tables read at the same size as the ones on their own page.
1892
+
1893
+ They were shrunk on the theory that a table inside a record is secondary.
1894
+ It is not — a work order's parts are the reason the record was opened, and
1895
+ setting them two points smaller than the same rows in a table view says the
1896
+ opposite. The only thing an embedded table gives up is the surrounding
1897
+ chrome. */
1898
+ .grid-compact table { font-size: inherit; }
1899
+
1900
+ .child-pager {
1901
+ display: flex; align-items: center; gap: 8px;
1902
+ padding: 10px 2px 0;
1903
+ }
1904
+ .child-range {
1905
+ margin-right: auto;
1906
+ font-size: 11px; color: var(--text-dim);
1907
+ font-variant-numeric: tabular-nums;
1908
+ }
1909
+
1910
+ /* ---------- record layout builder ---------- */
1911
+
1912
+ .record-heading { display: flex; align-items: center; gap: 10px; }
1913
+ .record-layout {
1914
+ margin-left: auto; padding: 3px 10px; font-size: 11px;
1915
+ display: inline-flex; align-items: center; gap: 7px;
1916
+ }
1917
+ .record-layout.is-custom { border-color: var(--accent); color: var(--accent); }
1918
+ .record-layout-name {
1919
+ padding-left: 7px; border-left: 1px solid var(--border);
1920
+ color: var(--text-dim); max-width: 12rem;
1921
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1922
+ }
1923
+
1924
+ /* The layout's own name, edited in place in the builder's header. */
1925
+ .tpl-title { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
1926
+ .tpl-eyebrow {
1927
+ font: 10px var(--mono); text-transform: uppercase; letter-spacing: 0.07em;
1928
+ color: var(--text-faint);
1929
+ }
1930
+ .tpl-name {
1931
+ flex: 1 1 8rem; min-width: 6rem;
1932
+ padding: 2px 6px; margin: 0 -6px;
1933
+ border: 1px solid transparent; border-radius: 5px;
1934
+ background: none; color: var(--text);
1935
+ font: 600 15px var(--mono); letter-spacing: -0.01em;
1936
+ }
1937
+ .tpl-name:hover { border-color: var(--border); }
1938
+ .tpl-name:focus { outline: none; border-color: var(--accent); background: var(--bg); }
1939
+ .tpl-for { font: 11px var(--mono); color: var(--text-faint); }
1940
+
1941
+ /* A drawer, not an overlay.
1942
+
1943
+ The builder used to cover the screen, which meant configuring a record
1944
+ layout while unable to see the record. Every change had to be committed
1945
+ before it could be judged. As a drawer the record stays beside it and
1946
+ re-renders on every toggle, so the preview is the actual page rather than
1947
+ a representation of it. */
1948
+ .tpl {
1949
+ position: fixed; top: 0; right: 0; bottom: 0; z-index: 60;
1950
+ width: min(420px, 42vw);
1951
+ background: var(--surface);
1952
+ border-left: 1px solid var(--border-strong);
1953
+ box-shadow: -8px 0 24px rgb(0 0 0 / 0.10);
1954
+ display: flex; flex-direction: column;
1955
+ }
1956
+ /* The page gives up the width rather than being covered by it. */
1957
+ body.editing-layout .layout { padding-right: min(420px, 42vw); }
1958
+
1959
+ .tpl-head {
1960
+ padding: 14px 16px 12px;
1961
+ border-bottom: 1px solid var(--border);
1962
+ display: flex; flex-direction: column; gap: 6px;
1963
+ }
1964
+ .tpl-head h2 { margin: 0; font: 600 13.5px var(--sans); letter-spacing: -0.01em; }
1965
+ .tpl-lede { margin: 0; color: var(--text-dim); font-size: 12px; line-height: 1.45; }
1966
+ .tpl-head-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 2px; }
1967
+ .tpl-head-actions button { padding: 5px 10px; font-size: 12px; }
1968
+
1969
+ .tpl-body { flex: 1; overflow-y: auto; padding: 12px 14px 32px; }
1970
+
1971
+ .tpl-section { border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; background: var(--bg); }
1972
+ .tpl-section.off { opacity: 0.55; }
1973
+ .tpl-section-head {
1974
+ display: flex; align-items: center; gap: 10px;
1975
+ padding: 10px 12px;
1976
+ border-bottom: 1px solid var(--border);
1977
+ }
1978
+ .tpl-section.off .tpl-section-head { border-bottom: none; }
1979
+ .tpl-toggle { display: flex; align-items: center; gap: 8px; cursor: pointer; }
1980
+ .tpl-section-name { font: 600 12.5px var(--sans); color: var(--text); }
1981
+ .tpl-section-kind { font: 10.5px var(--mono); color: var(--text-faint); }
1982
+ .tpl-section-actions { margin-left: auto; display: flex; gap: 2px; }
1983
+ .tpl-move {
1984
+ background: none; border: none; border-radius: 4px;
1985
+ color: var(--text-dim); cursor: pointer; padding: 3px 7px; font-size: 11px;
1986
+ }
1987
+ .tpl-move:hover:not([disabled]) { color: var(--accent); background: var(--accent-soft); }
1988
+ .tpl-move[disabled] { color: var(--text-faint); cursor: default; }
1989
+
1990
+ /* The shared arranger (page.css, `.pb-*`) inside a layout card.
1991
+
1992
+ It is drawn there to hang off a builder row — no top border, its own tint,
1993
+ rounded at the bottom only — and a card already provides all three. So it
1994
+ keeps its contents and gives up its box. */
1995
+ .tpl-section .pb-panel {
1996
+ margin: 0; padding: 8px 10px 10px;
1997
+ background: none; border: none; border-radius: 0;
1998
+ }
1999
+ .tpl-section .pb-arrange { max-height: none; }
2000
+
2001
+ .tpl-rows {
2002
+ display: flex; align-items: center; gap: 8px;
2003
+ padding: 4px 12px 12px; font-size: 12px; color: var(--text-dim);
2004
+ }
2005
+ .tpl-rows input {
2006
+ width: 4.5rem; padding: 4px 8px;
2007
+ font: 12px var(--mono); color: var(--text);
2008
+ background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
2009
+ }
2010
+
2011
+ /* ---------- agent console ---------- */
2012
+
2013
+ .agent-body { display: grid; grid-template-columns: 15rem 1fr; gap: 26px; padding-top: 18px; }
2014
+ .agent-tools { display: flex; flex-direction: column; gap: 2px; align-self: start; }
2015
+ .agent-tool {
2016
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
2017
+ padding: 6px 10px; border: none; background: none; border-radius: 6px;
2018
+ font: 13px var(--mono); color: var(--text); cursor: pointer; text-align: left;
2019
+ }
2020
+ .agent-tool:hover { background: var(--surface-2); }
2021
+ .agent-tool.active { background: var(--accent-soft); color: var(--accent); box-shadow: inset 3px 0 0 var(--accent); }
2022
+ .agent-badge {
2023
+ font: 600 9.5px var(--mono); letter-spacing: 0.05em; text-transform: uppercase;
2024
+ padding: 1px 6px; border-radius: 8px; border: 1px solid var(--border);
2025
+ color: var(--text-dim);
2026
+ }
2027
+ .agent-badge.tone-ok { color: var(--accent); border-color: var(--accent-soft); }
2028
+ .agent-badge.tone-warn { color: var(--warn); }
2029
+ .agent-badge.tone-bad { color: var(--danger); }
2030
+ .agent-desc-row { display: flex; align-items: center; gap: 8px; }
2031
+ .agent-desc-row h3 { margin: 0; font: 600 15px var(--mono); }
2032
+ .agent-desc { margin: 6px 0 14px; max-width: 68ch; font-size: 12.5px; color: var(--text-dim); }
2033
+ .agent-field { display: grid; grid-template-columns: 10rem 1fr; gap: 10px; align-items: start; margin-bottom: 8px; max-width: 44rem; }
2034
+ .agent-field label { font: 12.5px var(--mono); padding-top: 6px; }
2035
+ .agent-field label .req { margin-left: 6px; }
2036
+ .agent-input {
2037
+ padding: 5px 8px; font: 13px var(--mono);
2038
+ background: var(--surface); color: var(--text);
2039
+ border: 1px solid var(--border); border-radius: 6px; min-width: 0; width: 100%;
2040
+ }
2041
+ .agent-input:focus { outline: none; border-color: var(--accent); }
2042
+ .agent-run-row { margin: 10px 0 16px; }
2043
+ .agent-stats { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 6px; }
2044
+ .agent-answer {
2045
+ margin: 0; padding: 14px 16px; overflow: auto; max-height: 46vh;
2046
+ background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
2047
+ font: 12px/1.55 var(--mono); color: var(--text);
2048
+ }
2049
+ .agent-answer-error { color: var(--danger); border-color: var(--danger); }
2050
+ .agent-error { color: var(--danger); font: 12.5px var(--mono); }
2051
+ .agent-links { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
2052
+ .agent-transcript { margin-top: 22px; border-top: 1px solid var(--border); padding-top: 10px; }
2053
+ .agent-transcript h4 { margin: 0 0 6px; font: 600 11px var(--mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); }
2054
+ .agent-past {
2055
+ display: flex; gap: 12px; align-items: baseline; width: 100%;
2056
+ padding: 4px 8px; border: none; background: none; border-radius: 6px;
2057
+ font: 12px var(--mono); color: var(--text-dim); cursor: pointer; text-align: left;
2058
+ }
2059
+ .agent-past:hover { background: var(--surface-2); }
2060
+ .agent-past-tool { color: var(--text); font-weight: 600; flex: none; }
2061
+ .agent-past-args { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2062
+ .agent-past-cost { margin-left: auto; flex: none; color: var(--text-faint); }
2063
+ @media (max-width: 800px) {
2064
+ .agent-body { grid-template-columns: 1fr; }
2065
+ .agent-tools { flex-direction: row; flex-wrap: wrap; }
2066
+ .agent-field { grid-template-columns: 1fr; gap: 2px; }
2067
+ }
2068
+
2069
+ /* ---------- connection manager ---------- */
2070
+
2071
+ /* A place, not a dropdown. Full-screen because managing connections is a task
2072
+ you finish and leave rather than something you do in passing. */
2073
+ .conn-manager {
2074
+ position: fixed; inset: 0; z-index: 80;
2075
+ background: var(--bg);
2076
+ overflow-y: auto;
2077
+ }
2078
+ .conn-inner { max-width: 1040px; margin: 0 auto; padding: 34px 26px 60px; }
2079
+
2080
+ .conn-head {
2081
+ display: flex; align-items: flex-start; gap: 20px;
2082
+ padding-bottom: 18px;
2083
+ border-bottom: 1px solid var(--border);
2084
+ }
2085
+ .diagram-scope { font-size: 11px; padding: 1px 8px; }
2086
+
2087
+ .conn-brand { display: flex; align-items: center; gap: 7px; margin-bottom: 14px; }
2088
+ .conn-brand-name { font: 600 13px var(--mono); letter-spacing: -0.01em; color: var(--text-dim); }
2089
+ .conn-head h2 { margin: 0 0 4px; font: 600 19px var(--mono); letter-spacing: -0.015em; }
2090
+ .conn-lede { margin: 0; font-size: 12.5px; color: var(--text-dim); max-width: 46ch; }
2091
+ .conn-head-actions { margin-left: auto; display: flex; gap: 8px; flex: none; }
2092
+
2093
+ .conn-manage {
2094
+ flex: none; display: inline-flex; align-items: center; justify-content: center;
2095
+ width: 28px; height: 28px; line-height: 1;
2096
+ background: none; border: 1px solid transparent; border-radius: 6px;
2097
+ color: var(--text-dim); cursor: pointer;
2098
+ }
2099
+ .conn-manage:hover { color: var(--text); border-color: var(--border); background: var(--surface-2); }
2100
+
2101
+ .conn-toolbar { display: flex; align-items: center; gap: 12px; padding: 16px 0 8px; }
2102
+ .conn-filter {
2103
+ flex: 1; max-width: 22rem; padding: 6px 10px;
2104
+ border: 1px solid var(--border); border-radius: 6px;
2105
+ background: var(--surface); color: var(--text);
2106
+ font: 12.5px var(--mono);
2107
+ }
2108
+ .conn-filter:focus { outline: none; border-color: var(--accent); }
2109
+ .conn-count { font: 11px var(--mono); color: var(--text-faint); }
2110
+
2111
+ /* One connection, one line.
2112
+
2113
+ Cards were legible and did not scale: at 170px each, fifty connections is
2114
+ eight thousand pixels of scrolling to find one name. Nothing was dropped to
2115
+ get to forty — what moved is the actions, which are the same four on every
2116
+ row and so do not need fifty permanent copies. */
2117
+ .conn-list { display: flex; flex-direction: column; }
2118
+
2119
+ .conn-row {
2120
+ display: grid;
2121
+ grid-template-columns: 8px minmax(8rem, auto) minmax(0, 1fr) auto 6rem auto;
2122
+ align-items: center; gap: 14px;
2123
+ padding: 9px 12px;
2124
+ border-bottom: 1px solid var(--border);
2125
+ cursor: pointer;
2126
+ }
2127
+ .conn-row:hover { background: var(--surface); }
2128
+ .conn-row.cursor, .conn-row:focus-visible {
2129
+ outline: none;
2130
+ background: var(--accent-soft);
2131
+ box-shadow: inset 3px 0 0 var(--accent);
2132
+ }
2133
+ .conn-row.active { background: var(--accent-soft); }
2134
+ .conn-row.failed { background: color-mix(in srgb, var(--danger) 5%, transparent); }
2135
+
2136
+ /* State as a dot: three values that need to be scannable down a column of
2137
+ fifty, where a word would be read fifty times. The word is still there at
2138
+ the end of the row for anyone who wants it. */
2139
+ .conn-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-faint); }
2140
+ .conn-dot.ok { background: var(--accent); }
2141
+ .conn-dot.bad { background: var(--danger); }
2142
+ .conn-dot.idle { background: var(--border-strong); }
2143
+ /* Reading a schema is a wait worth showing: a Postgres catalog query across
2144
+ sixty tables takes long enough to notice, and a remote database much
2145
+ longer. */
2146
+ .conn-dot.busy { background: var(--key); animation: conn-pulse 1s ease-in-out infinite; }
2147
+ @keyframes conn-pulse { 50% { opacity: 0.25; } }
2148
+ @media (prefers-reduced-motion: reduce) { .conn-dot.busy { animation: none; } }
2149
+
2150
+ .conn-name { display: flex; align-items: baseline; gap: 7px; min-width: 0; }
2151
+ .conn-name-text {
2152
+ font: 13px var(--mono); color: var(--text);
2153
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
2154
+ }
2155
+ .conn-row.active .conn-name-text { color: var(--accent); font-weight: 600; }
2156
+
2157
+ .conn-detail {
2158
+ font: 11.5px var(--mono); color: var(--text-faint);
2159
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
2160
+ }
2161
+ .conn-meta { display: flex; gap: 5px; flex: none; }
2162
+ .conn-badge {
2163
+ padding: 1px 6px; border-radius: 999px;
2164
+ border: 1px solid var(--border); color: var(--text-faint);
2165
+ font: 9.5px var(--mono); text-transform: uppercase; letter-spacing: 0.05em;
2166
+ white-space: nowrap;
2167
+ }
2168
+ .conn-badge.current { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
2169
+ .conn-badge.source-session { border-color: var(--key); color: var(--key); }
2170
+ /* A boundary, not a status: coloured like the other warnings so it reads as
2171
+ a fact about what this connection will do rather than a label. */
2172
+ .conn-badge.shape-only { border-color: var(--warn, var(--key)); color: var(--warn, var(--key)); }
2173
+
2174
+ .conn-status { font: 11px var(--mono); text-align: right; }
2175
+ .conn-status.ok { color: var(--text-dim); }
2176
+ .conn-status.bad { color: var(--danger); }
2177
+ .conn-status.idle { color: var(--text-faint); }
2178
+ .conn-status.busy { color: var(--key); }
2179
+
2180
+ /* Revealed on approach. Kept in the layout at all times so the row does not
2181
+ reflow when they appear — only their visibility changes. */
2182
+ .conn-actions { display: flex; gap: 4px; flex: none; visibility: hidden; }
2183
+ .conn-row:hover .conn-actions,
2184
+ .conn-row:focus-within .conn-actions,
2185
+ .conn-row.cursor .conn-actions { visibility: visible; }
2186
+ .conn-action {
2187
+ padding: 3px 8px; border-radius: 5px;
2188
+ background: none; border: 1px solid var(--border);
2189
+ color: var(--text-dim); cursor: pointer;
2190
+ font: 11px var(--mono); white-space: nowrap;
2191
+ }
2192
+ .conn-action:hover { color: var(--accent); border-color: var(--accent); background: var(--surface); }
2193
+ /* The action becomes a label while it runs, so it cannot be pressed twice and
2194
+ the row does not change width when it does. */
2195
+ .conn-action.working { border-color: transparent; color: var(--key); cursor: default; }
2196
+ /* Kept visible while working even without a hover, or the row would go quiet
2197
+ the moment the pointer left it. */
2198
+ .conn-row.busy .conn-actions { visibility: visible; }
2199
+ .conn-remove { color: var(--danger); }
2200
+ .conn-remove:hover { color: var(--danger); border-color: var(--danger); background: color-mix(in srgb, var(--danger) 10%, transparent); }
2201
+
2202
+ /* The reason a row is red, under it. A one-line list has nowhere else to put
2203
+ an error, and an error hidden in a tooltip is an error nobody reads. */
2204
+ .conn-error, .conn-warning {
2205
+ grid-column: 2 / -1;
2206
+ margin: 2px 0 4px; font: 11.5px var(--mono);
2207
+ }
2208
+ .conn-error { color: var(--danger); }
2209
+ .conn-warning { color: var(--key); }
2210
+
2211
+ .conn-note {
2212
+ margin-top: 26px; padding-top: 18px;
2213
+ border-top: 1px solid var(--border);
2214
+ font-size: 12px; color: var(--text-dim);
2215
+ }
2216
+ .conn-note p { margin: 0 0 8px; }
2217
+ .conn-note ul { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 5px; }
2218
+ .conn-note li { font-family: var(--mono); font-size: 11.5px; color: var(--text-faint); }
2219
+
2220
+ /* ---------- deleting a row ---------- */
2221
+
2222
+ .record-delete { color: var(--danger); font-size: 11px; padding: 3px 10px; }
2223
+ .record-delete:hover { border-color: var(--danger); background: color-mix(in srgb, var(--danger) 10%, transparent); }
2224
+
2225
+ /* The impact report. Each line leads with the consequence rather than the
2226
+ count, because "3 rows in invoice_line" is not an answer to "what happens if
2227
+ I delete this" and "3 rows will be deleted too" is. */
2228
+ .delete-impact { margin: 10px 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 6px; }
2229
+ .delete-impact li {
2230
+ display: flex; align-items: baseline; gap: 10px;
2231
+ padding: 7px 10px; border-radius: 6px;
2232
+ border-left: 3px solid var(--border);
2233
+ background: var(--surface-2);
2234
+ font: 12px/1.5 var(--mono);
2235
+ }
2236
+ .impact-what { flex: 1; }
2237
+ .impact-rule { font-size: 10px; color: var(--text-faint); white-space: nowrap; }
2238
+ /* Three tones for three outcomes, so the shape of the answer reads before the
2239
+ words do. */
2240
+ .impact-bad { border-left-color: var(--danger); }
2241
+ .impact-bad .impact-what { color: var(--danger); }
2242
+ .impact-warn { border-left-color: var(--key); }
2243
+ .impact-stop { border-left-color: var(--text-dim); }
2244
+ .impact-unknown { border-left-color: var(--text-faint); }
2245
+
2246
+ .delete-note, .delete-blocked {
2247
+ margin: 10px 0 0; font-size: 11.5px; color: var(--text-dim); line-height: 1.5;
2248
+ }
2249
+ .delete-blocked { color: var(--key); }
2250
+ .delete-confirm { max-width: 34rem; }
2251
+
2252
+ /* A toast can carry one thing to do about it — see `toast` in core.js. */
2253
+ .toast { display: flex; align-items: center; gap: 12px; }
2254
+ /* Restores what `display: flex` took away.
2255
+
2256
+ `hidden` is how core.js dismisses a toast, and it works by way of the user
2257
+ agent's `[hidden] { display: none }` — which any `display` declaration in a
2258
+ stylesheet outranks. So every toast was hidden in the accessibility tree,
2259
+ still on screen, and stayed there until the next one replaced it. */
2260
+ .toast[hidden] { display: none; }
2261
+ .toast-action {
2262
+ flex: none; padding: 3px 10px; border-radius: 5px;
2263
+ background: none; border: 1px solid currentColor;
2264
+ color: inherit; cursor: pointer; font: 11.5px var(--mono);
2265
+ }
2266
+ .toast-action:hover { background: color-mix(in srgb, currentColor 15%, transparent); }
2267
+
2268
+ /* ---------- a row's children, opened in place ---------- */
2269
+
2270
+ .expander-cell { width: 1.9rem; padding-left: 10px; padding-right: 0; }
2271
+ .expander {
2272
+ padding: 0 4px; line-height: 1;
2273
+ background: none; border: none;
2274
+ color: var(--text-faint); cursor: pointer;
2275
+ font-size: 10px;
2276
+ }
2277
+ .expander:hover { color: var(--accent); }
2278
+ tr.expanded .expander { color: var(--accent); }
2279
+ /* The row stays visibly attached to what it opened. */
2280
+ tr.expanded > td { border-bottom-color: transparent; background: var(--accent-soft); }
2281
+
2282
+ .detail-row > td { padding: 0; background: var(--surface-2); }
2283
+ .detail-cell {
2284
+ padding: 12px 16px 16px 34px !important;
2285
+ border-bottom: 1px solid var(--border-strong);
2286
+ /* Its own scroll, so a wide child table does not widen the parent — every
2287
+ column of which would then be laid out around data it has nothing to do
2288
+ with. */
2289
+ max-width: 100%;
2290
+ }
2291
+ .detail-section + .detail-section { margin-top: 16px; }
2292
+ .detail-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 6px; }
2293
+ .detail-head h4 {
2294
+ margin: 0; font: 600 11px var(--mono);
2295
+ text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim);
2296
+ }
2297
+ .detail-via { font: 10px var(--mono); color: var(--text-faint); }
2298
+ .detail-count {
2299
+ font: 11px var(--mono); color: var(--accent);
2300
+ font-variant-numeric: tabular-nums;
2301
+ }
2302
+ /* Beside the count, not banished to the far right.
2303
+
2304
+ `margin-left: auto` pushed it to the edge of a full-width head, which was
2305
+ fine when the child table underneath was also full width. Now that a child
2306
+ table is only as wide as it needs, a "See all" a thousand pixels to the
2307
+ right of the last column belongs to nothing the eye can see — it reads as
2308
+ page furniture rather than as this section's control. */
2309
+ .detail-all { margin-left: 4px; font-size: 11px; padding: 2px 8px; }
2310
+ /* The inner grid keeps its own inset rather than inheriting the page's. */
2311
+ .detail-cell .table-wrap { padding: 0; }
2312
+ /* The child's width must not reach the parent.
2313
+
2314
+ A table sized to its contents inside a cell with `colspan` hands its
2315
+ preferred width *up*: a thirteen-column child made the outer table 2834px
2316
+ wide and put a horizontal scrollbar on the whole page. `contain:
2317
+ inline-size` sizes this box from its parent as though it had no contents,
2318
+ so a wide child scrolls inside its own strip and the result above it stays
2319
+ where it was. */
2320
+ .detail-cell .table-wrap { contain: inline-size; }
2321
+
2322
+ /* A child table is as wide as its contents, not as wide as the window.
2323
+
2324
+ `width: 100%` is right for the result you asked for and wrong for a table
2325
+ nested inside it: six narrow columns stretched across seventeen hundred
2326
+ pixels put `ID` in the middle of the row with nothing either side of it,
2327
+ and four such tables under one row each spread their columns to different
2328
+ places. Sized to content they pack left, start at the same edge, and read
2329
+ as a group of small tables rather than as four sparse ones.
2330
+
2331
+ Wider than the container is still possible — a child with twenty columns —
2332
+ and `.table-wrap` already scrolls, which is what the outer table does too. */
2333
+ /* As wide as its contents, but never wider than the row it sits in.
2334
+
2335
+ `width: auto` alone let a thirteen-column child sprawl to nearly three
2336
+ thousand pixels, giving every expanded row its own horizontal scrollbar to
2337
+ fight. The cap brings the wide ones back to the container, where their
2338
+ columns compress exactly as a top-level result's do, and leaves the narrow
2339
+ ones — the common case, and the one that looked scattered — at their own
2340
+ width. */
2341
+ .detail-cell table { width: auto; max-width: 100%; }
2342
+ /* Tighter than the 30rem a top-level result allows. A description column that
2343
+ takes half the width of a nested table is the same spreading problem in one
2344
+ column instead of all of them. */
2345
+ .detail-cell th, .detail-cell td { max-width: 18rem; }
2346
+
2347
+ /* A child table's header is not a peer of the page's.
2348
+
2349
+ Both were `--surface` on `--border-strong`, so a nested header read as
2350
+ another top-level one and the two competed for the same rank — worst where
2351
+ an expanded row sits directly under the real header and the eye has to work
2352
+ out which columns belong to which table. The inner one takes the tinted
2353
+ ground it is sitting on and a hairline in the ordinary border colour: still
2354
+ legibly a header, plainly the quieter of the two. */
2355
+ .detail-cell thead th {
2356
+ /* A step away from the ground it sits on, not the same colour as it.
2357
+
2358
+ Plain `--surface-2` *is* the expanded row's background, so the header
2359
+ dissolved into its own container and the child table lost its top edge.
2360
+ Mixed toward the text colour instead, which darkens it in the light
2361
+ theme and lightens it in the dark one — a raised strip either way, and
2362
+ one step quieter than the page's own header rather than level with it. */
2363
+ background: color-mix(in srgb, var(--text) 7%, var(--surface-2));
2364
+ border-bottom: 1px solid var(--border-strong);
2365
+ color: var(--text-dim);
2366
+ font-size: 10.5px;
2367
+ }
2368
+ .detail-cell thead th.sorted { color: var(--accent); border-bottom-color: var(--accent); }
2369
+ /* Sticky belongs to the page's header, not to a table nested three rows down:
2370
+ an inner header pinning itself to the top of the scroller detaches from the
2371
+ rows it names. */
2372
+ .detail-cell thead th { position: static; }
2373
+ .detail-cell .pinned { background: color-mix(in srgb, var(--text) 7%, var(--surface-2)); }
2374
+
2375
+ /* ---------- what is in this database ---------- */
2376
+
2377
+ /* Barely rounded. A 10px radius on a card full of monospace tables reads as
2378
+ a decade-old dashboard; the tabs went square for the same reason, and a
2379
+ hairline of curvature is enough to say "card" without saying "widget". */
2380
+ /* Room to breathe. Twelve pixels of padding around eleven lines of 10px
2381
+ monospace reads as congested however carefully the lines are spaced. */
2382
+ .shape {
2383
+ max-width: 62rem;
2384
+ margin: 0 0 16px;
2385
+ padding: 16px 20px;
2386
+ background: var(--surface);
2387
+ border: 1px solid var(--border);
2388
+ border-radius: 3px;
2389
+ }
2390
+ .shape-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
2391
+ .shape-body { margin-top: 12px; }
2392
+
2393
+ /* The title is the control. A separate chevron would be one more thing in a
2394
+ row whose whole problem is how much it holds; the row already reads as the
2395
+ card's header, so clicking it folds the card. */
2396
+ .shape-toggle {
2397
+ margin: 0; padding: 0; border: none; background: none; cursor: pointer;
2398
+ font: 600 16px var(--mono); letter-spacing: -0.015em; color: var(--text);
2399
+ display: flex; align-items: baseline; gap: 7px;
2400
+ }
2401
+ .shape-toggle::before {
2402
+ content: "▾"; font-size: 10px; color: var(--text-faint);
2403
+ }
2404
+ /* The chevron still turns when motion is reduced — it is a state indicator,
2405
+ not decoration. Only the turning is animated, and only where animation is
2406
+ welcome. */
2407
+ @media (prefers-reduced-motion: no-preference) {
2408
+ .shape-toggle::before { transition: transform var(--t-hover) var(--ease-out); }
2409
+ }
2410
+ .shape-toggle:hover { color: var(--accent); }
2411
+ .shape-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
2412
+ .shape-counts { font: 11px var(--mono); color: var(--text-faint); font-variant-numeric: tabular-nums; }
2413
+ .shape-start { margin-left: auto; font-size: 11.5px; }
2414
+
2415
+ .shape-closed .shape-body { display: none; }
2416
+ .shape-closed .shape-toggle::before { transform: rotate(-90deg); }
2417
+ /* Collapsed, the card is one line, so the padding that framed five lines of
2418
+ prose reads as a box drawn around a sentence. */
2419
+ .shape-closed { padding: 9px 16px; }
2420
+ /* Only worth saying when the lines that said it in full are hidden. */
2421
+ .shape-gist { display: none; font: 11.5px var(--mono); color: var(--text-dim); }
2422
+ .shape-closed .shape-gist { display: inline; }
2423
+
2424
+ .shape-lines { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 9px; }
2425
+ /* A label column, so the four claims read as four answers to four questions
2426
+ rather than as a paragraph that happens to have been split. */
2427
+ .shape-line { display: grid; grid-template-columns: 9.5rem 1fr; gap: 14px; align-items: baseline; }
2428
+ @media (max-width: 640px) { .shape-line { grid-template-columns: 1fr; gap: 2px; } }
2429
+ .shape-label {
2430
+ font: 10px var(--mono); text-transform: uppercase; letter-spacing: 0.06em;
2431
+ color: var(--text-faint); text-align: right;
2432
+ }
2433
+ @media (max-width: 640px) { .shape-label { text-align: left; } }
2434
+ .shape-says { font: 12.5px/1.55 var(--mono); color: var(--text-dim); }
2435
+
2436
+ /* Every table named is a way in. A summary you cannot act on is a paragraph to
2437
+ read and then leave. */
2438
+ .shape-link {
2439
+ padding: 1px 5px; margin: 0 -1px; border-radius: 4px;
2440
+ background: none; border: none;
2441
+ color: var(--accent); cursor: pointer;
2442
+ font: inherit;
2443
+ }
2444
+ .shape-link:hover { background: var(--accent-soft); text-decoration: underline; }
2445
+ .shape-arrow { color: var(--text-faint); }
2446
+
2447
+ /* ---------- the keyboard cursor on a result ---------- */
2448
+
2449
+ /* One tab stop for the whole grid; the cursor moves inside it with the arrow
2450
+ keys. A hundred and sixty tab stops between the query bar and whatever is
2451
+ after it is not navigation. */
2452
+ .result table:focus { outline: none; }
2453
+ .result table:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
2454
+
2455
+ /* An inset ring rather than a background fill: the cell already carries
2456
+ meaning through colour — null is dim, numbers align right, references are
2457
+ chips — and painting over it to say "you are here" costs more than it
2458
+ says. */
2459
+ .cell-cursor {
2460
+ outline: 2px solid var(--accent);
2461
+ outline-offset: -2px;
2462
+ border-radius: 3px;
2463
+ }
2464
+ .cell-cursor:focus { outline-color: var(--accent); }
2465
+
2466
+ /* Copy needs an acknowledgement: the clipboard is somewhere else, so without
2467
+ one the key press has no visible effect at all. */
2468
+ .cell-copied { background: var(--accent-soft); }
2469
+ @media (prefers-reduced-motion: no-preference) {
2470
+ .cell-copied { transition: background var(--t-hover) var(--ease-out); }
2471
+ }
2472
+
2473
+
2474
+ /* The expand-every-row control, in the header cell above the expanders. It is
2475
+ the same glyph as the row's own, one rank quieter, so it reads as "all of
2476
+ these" rather than as a different kind of control. */
2477
+ .expand-all { color: var(--text-faint); }
2478
+ .expand-all:hover { color: var(--accent); }
2479
+ .expand-all.working { opacity: 0.5; cursor: progress; }
2480
+
2481
+ /* The trail's root. Quiet like the crumbs beside it — it is a step in the
2482
+ trail, not a button competing with one — but always present, which is the
2483
+ whole point of it. */
2484
+ /* Full-strength, not dimmed like the crumbs beside it. It is the one thing in
2485
+ the trail that is a control rather than a step, and the whole reason it was
2486
+ added is that the way home was not noticeable. */
2487
+ .crumb-home {
2488
+ display: inline-flex; align-items: center;
2489
+ /* The row is baseline-aligned, which is right for the text crumbs and
2490
+ meaningless for an icon — an SVG has no baseline to sit on, so it hung
2491
+ above the line it was supposed to be part of. Centred against the line
2492
+ box instead. */
2493
+ align-self: center;
2494
+ color: var(--text);
2495
+ padding: 2px 4px;
2496
+ border-radius: var(--radius);
2497
+ }
2498
+ /* Inline SVG sits on the text baseline by default and leaves descender space
2499
+ under it, which is the other half of the same misalignment. */
2500
+ .crumb-home svg { display: block; }
2501
+ .crumb-home:hover { background: var(--surface-2); }
2502
+ .crumb-home:hover { color: var(--accent); }
2503
+
2504
+ /* Clauses that describe a query which did not run.
2505
+
2506
+ Not an error colour — the banner above already says what is wrong, and
2507
+ painting the chips red would claim each of them is the fault. They are
2508
+ simply not in effect, and look it. The × on each still works: removing the
2509
+ clause that broke the query is the fix. */
2510
+ .views.not-in-effect .clause { opacity: 0.55; }
2511
+ .views .view-save[disabled] { opacity: 0.45; cursor: default; }
2512
+
2513
+ /* ---------- the connection dialog ---------- */
2514
+
2515
+ .conn-dialog { min-width: min(560px, 92vw); }
2516
+ /* Two ways of saying the same thing, so they read as one control with two
2517
+ faces rather than as two separate forms. */
2518
+ .conn-tabs { display: flex; gap: 4px; margin: 4px 0 12px; }
2519
+ .conn-tab {
2520
+ background: none; border: 1px solid transparent; border-radius: var(--radius);
2521
+ padding: 4px 10px; font: inherit; font-size: 12px;
2522
+ color: var(--text-dim); cursor: pointer;
2523
+ }
2524
+ .conn-tab:hover { color: var(--text); }
2525
+ .conn-tab.active { color: var(--text); border-color: var(--border-strong); background: var(--surface-2); }
2526
+ /* Host and port sit together, as they are read; the rest take a line each. */
2527
+ /* Host and port share a row, because they are read as one thing. Everything
2528
+ else takes the width.
2529
+
2530
+ Keyed on which field it is rather than on its position: this was written as
2531
+ `nth-child` while the driver picker was the first child, and moving that
2532
+ picker out of the grid shifted every index — so the rule went on describing
2533
+ a layout that no longer existed and paired the wrong two boxes. A selector
2534
+ that survives reordering is worth the `:has`. */
2535
+ .conn-parts { display: grid; grid-template-columns: 2fr 1fr; gap: 10px 12px; }
2536
+ .conn-parts > .field-stack { grid-column: 1 / -1; }
2537
+ .conn-parts > .field-stack:has(input[aria-label='Host']) { grid-column: 1 / 2; }
2538
+ .conn-parts > .field-stack:has(input[aria-label='Port']) { grid-column: 2 / 3; }
2539
+ /* A password source is a choice, so it sits with the field it decides. */
2540
+ .conn-secret-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
2541
+ .conn-tabs-small { margin: 0; }
2542
+ .conn-tabs-small .conn-tab { padding: 2px 7px; font-size: 11px; }
2543
+ .conn-secret-note { margin: 4px 0 0; font-size: 11px; color: var(--text-faint); }
2544
+
2545
+ .conn-preview-row {
2546
+ display: flex; gap: 8px; align-items: baseline;
2547
+ margin-top: 12px; font-size: 12px; color: var(--text-dim);
2548
+ }
2549
+
2550
+ /* `hidden` is a UA rule and any explicit `display` beats it, so the row above
2551
+ has to say so itself. It is set now that the row only appears when it has
2552
+ something to add — a password the box above is holding back. */
2553
+ .conn-preview-row[hidden] { display: none; }
2554
+ /* Wraps rather than scrolls: the point of the preview is being able to read
2555
+ the whole thing before saving it. */
2556
+ .conn-preview {
2557
+ font-family: var(--mono); color: var(--text);
2558
+ word-break: break-all; background: var(--surface-2);
2559
+ border: 1px solid var(--rule, var(--border)); border-radius: 3px; padding: 1px 5px;
2560
+ }
2561
+
2562
+ /* ---------- the query modal (⌘/) ---------- */
2563
+
2564
+ /* Same z-index as the palette, for the same reason: it is bound at the
2565
+ document with capture, so it can be opened over anything and must never
2566
+ open underneath it. */
2567
+ .ask-backdrop {
2568
+ position: fixed; inset: 0; z-index: 100;
2569
+ background: rgb(0 0 0 / 0.28);
2570
+ display: flex; justify-content: center;
2571
+ padding-top: 18vh;
2572
+ }
2573
+ /* Wider than the palette. The palette shows a list of short names; this holds
2574
+ a sentence, and the whole reason it exists is that the header's bar is too
2575
+ narrow for one. */
2576
+ .ask {
2577
+ width: min(760px, 94vw); height: fit-content;
2578
+ background: var(--surface);
2579
+ border: 1px solid var(--border-strong);
2580
+ border-radius: 10px;
2581
+ box-shadow: 0 18px 60px rgb(0 0 0 / 0.28);
2582
+ overflow: hidden;
2583
+ }
2584
+ .ask-input {
2585
+ display: block; width: 100%;
2586
+ padding: 14px 16px;
2587
+ border: none; border-bottom: 1px solid var(--border);
2588
+ background: none; color: var(--text);
2589
+ font: 14px/1.5 var(--mono);
2590
+ resize: none;
2591
+ }
2592
+ .ask-input:focus { outline: none; border-bottom-color: var(--accent); }
2593
+ .ask-foot {
2594
+ display: flex; align-items: center; justify-content: space-between; gap: 12px;
2595
+ padding: 8px 16px;
2596
+ font-size: 12px; color: var(--text-dim);
2597
+ }
2598
+ .ask-hint code {
2599
+ font-family: var(--mono);
2600
+ color: var(--text);
2601
+ }
2602
+ .ask-keys { color: var(--text-faint); white-space: nowrap; }
2603
+
2604
+ /* "or fill in the parts" — a rule with the words sitting in the gap.
2605
+
2606
+ The two shapes are alternatives, and a plain gap between them reads as one
2607
+ long form where the second half continues the first. The line says choose,
2608
+ without a control to click. */
2609
+ .conn-or {
2610
+ display: flex;
2611
+ align-items: center;
2612
+ gap: 10px;
2613
+ margin: 14px 0 12px;
2614
+ color: var(--text-dim);
2615
+ font-size: 12px;
2616
+ }
2617
+
2618
+ .conn-or::before,
2619
+ .conn-or::after {
2620
+ content: "";
2621
+ flex: 1;
2622
+ border-top: 1px solid var(--border);
2623
+ }
2624
+
2625
+ /* ---------- what the Test button answered ----------
2626
+
2627
+ This was a grey `<p class="note">` between two fields, in the same colour
2628
+ as the fine print under it — so the one line on the form that answers a
2629
+ question somebody just asked read as more static help. It is a block with
2630
+ a mark now, coloured by outcome, sitting against the buttons that produced
2631
+ it. */
2632
+ .conn-result {
2633
+ display: flex;
2634
+ gap: 9px;
2635
+ align-items: flex-start;
2636
+ margin: 14px 0 0;
2637
+ padding: 9px 11px;
2638
+ border: 1px solid var(--border);
2639
+ border-left-width: 3px;
2640
+ border-radius: var(--radius);
2641
+ background: var(--surface-2);
2642
+ font-size: 12.5px;
2643
+ line-height: 1.5;
2644
+ }
2645
+
2646
+ .conn-result[hidden] { display: none; }
2647
+ .conn-result.ok { border-left-color: var(--accent); }
2648
+ .conn-result.bad { border-left-color: var(--danger); }
2649
+
2650
+ .conn-result-mark { font-weight: 700; line-height: 1.4; }
2651
+ .conn-result.ok .conn-result-mark { color: var(--accent); }
2652
+ .conn-result.bad .conn-result-mark { color: var(--danger); }
2653
+
2654
+ .conn-result-text { margin: 0; color: var(--text); }
2655
+
2656
+ /* The warning the server works out and the form used to drop on the floor.
2657
+ "Password from inline" is a fact; this is why anyone should care. */
2658
+ .conn-result-detail {
2659
+ margin: 3px 0 0;
2660
+ color: var(--warn);
2661
+ font-size: 12px;
2662
+ }
2663
+
2664
+ /* A line under a field saying what it is for, rather than a placeholder that
2665
+ vanishes the moment somebody types into it. */
2666
+ .field-hint {
2667
+ display: block;
2668
+ margin-top: 4px;
2669
+ font-size: 11.5px;
2670
+ line-height: 1.45;
2671
+ color: var(--text-dim);
2672
+ }
2673
+
2674
+ /* A paste with no `://` in it. The name tablewalk prints for a connection is
2675
+ its public URL form, so pasting one back is an ordinary thing to do — and
2676
+ it lands in the file-path branch. Which driver it wants is the one thing
2677
+ the string does not say, so it is asked rather than guessed. */
2678
+ .conn-scheme-hint {
2679
+ display: flex;
2680
+ flex-wrap: wrap;
2681
+ align-items: center;
2682
+ gap: 6px;
2683
+ margin: 5px 0 0;
2684
+ font-size: 11.5px;
2685
+ color: var(--warn);
2686
+ }
2687
+
2688
+ .conn-scheme-hint[hidden] { display: none; }
2689
+
2690
+ .conn-scheme-fix {
2691
+ font: 11.5px var(--mono);
2692
+ padding: 1px 7px;
2693
+ border: 1px solid var(--border-strong);
2694
+ border-radius: 4px;
2695
+ background: var(--surface);
2696
+ color: var(--text);
2697
+ cursor: pointer;
2698
+ }
2699
+
2700
+ .conn-scheme-fix:hover { border-color: var(--accent); color: var(--accent); }
2701
+
2702
+ /* The note under the form about what happens to this connection, and to the
2703
+ password in it. It was `--text-faint`, a mid-grey at half alpha meant for a
2704
+ rule between things — too quiet for a line that carries a guarantee about
2705
+ where a secret ends up. */
2706
+ .conn-fineprint {
2707
+ margin: 10px 0 0;
2708
+ font-size: 11.5px;
2709
+ line-height: 1.5;
2710
+ color: var(--text-dim);
2711
+ }