@mason-data/cli 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +90 -71
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog.js +222 -13
  4. package/dist/src/api/postgres/encode.js +3 -1
  5. package/dist/src/api/postgres/explain.js +3 -2
  6. package/dist/src/api/postgres/protocol.js +39 -23
  7. package/dist/src/api/postgres/server.js +1173 -207
  8. package/dist/src/api/postgres/translate.js +81 -9
  9. package/dist/src/api/request-log.js +18 -5
  10. package/dist/src/cdc/poll.js +5 -5
  11. package/dist/src/cli/background.js +29 -6
  12. package/dist/src/cli/cancel.js +75 -0
  13. package/dist/src/cli/command.js +1 -0
  14. package/dist/src/cli/commands.js +622 -183
  15. package/dist/src/cli/control.js +59 -0
  16. package/dist/src/cli/demo.js +19 -10
  17. package/dist/src/cli/dev.js +351 -111
  18. package/dist/src/cli/doctor.js +20 -10
  19. package/dist/src/cli/guides.js +79 -31
  20. package/dist/src/cli/init.js +119 -58
  21. package/dist/src/cli/machine.js +12 -11
  22. package/dist/src/cli/main.js +3 -3
  23. package/dist/src/cli/mcp.js +29 -20
  24. package/dist/src/cli/metrics.js +79 -0
  25. package/dist/src/cli/pg-tls.js +13 -3
  26. package/dist/src/cli/pg-url.js +4 -0
  27. package/dist/src/cli/pin.js +13 -12
  28. package/dist/src/cli/preflight.js +8 -7
  29. package/dist/src/cli/project.js +43 -4
  30. package/dist/src/cli/readiness.js +38 -4
  31. package/dist/src/cli/registry.js +53 -34
  32. package/dist/src/cli/replication-checks.js +7 -5
  33. package/dist/src/cli/replication-mode.js +105 -26
  34. package/dist/src/cli/request-filter.js +134 -0
  35. package/dist/src/cli/roles.js +584 -0
  36. package/dist/src/cli/run-server.js +483 -41
  37. package/dist/src/cli/runs.js +1 -1
  38. package/dist/src/cli/security.js +10 -8
  39. package/dist/src/cli/serve.js +144 -42
  40. package/dist/src/cli/share-run.js +286 -0
  41. package/dist/src/cli/share.js +286 -0
  42. package/dist/src/cli/source-edit.js +65 -13
  43. package/dist/src/cli/sources.js +287 -38
  44. package/dist/src/cli/supervise.js +12 -1
  45. package/dist/src/cli/sync.js +55 -25
  46. package/dist/src/cli/tunnel.js +200 -0
  47. package/dist/src/cli/ui.js +9 -6
  48. package/dist/src/cli/usage.js +128 -0
  49. package/dist/src/cli/user.js +496 -40
  50. package/dist/src/cli/version.js +9 -7
  51. package/dist/src/demo/seed.js +3 -0
  52. package/dist/src/duck/cancel.js +10 -1
  53. package/dist/src/duck/events.js +134 -0
  54. package/dist/src/duck/executor-worker.js +18 -9
  55. package/dist/src/duck/executor.js +43 -30
  56. package/dist/src/duck/explain.js +56 -46
  57. package/dist/src/duck/extensions.js +160 -0
  58. package/dist/src/duck/functions.js +13 -14
  59. package/dist/src/duck/gaps.js +17 -8
  60. package/dist/src/duck/grants.js +176 -0
  61. package/dist/src/duck/history.js +123 -0
  62. package/dist/src/duck/kept.js +6 -6
  63. package/dist/src/duck/plan.js +102 -25
  64. package/dist/src/duck/publication.js +6 -6
  65. package/dist/src/duck/requests.js +265 -93
  66. package/dist/src/duck/rewrite.js +25 -5
  67. package/dist/src/duck/row-filter.js +649 -0
  68. package/dist/src/duck/store.js +156 -120
  69. package/dist/src/duck/stream.js +10 -5
  70. package/dist/src/duck/sync.js +835 -268
  71. package/dist/src/duck/types.js +6 -5
  72. package/dist/src/duck/usage.js +324 -0
  73. package/dist/src/duck/users.js +64 -15
  74. package/dist/src/duck/verified.js +3 -0
  75. package/dist/src/duck/views.js +2 -3
  76. package/dist/src/lib/database-name.js +62 -0
  77. package/dist/src/lib/dotenv.js +17 -4
  78. package/dist/src/lib/log-buffer.js +68 -0
  79. package/dist/src/lib/pg-numeric.js +5 -2
  80. package/dist/src/lib/project-version.js +23 -23
  81. package/dist/src/lib/table-sync.js +267 -0
  82. package/dist/src/web/app.css +706 -66
  83. package/dist/src/web/app.js +327 -410
  84. package/dist/src/web/charts.js +229 -0
  85. package/dist/src/web/index.html +82 -18
  86. package/dist/src/web/pages/queries.js +192 -0
  87. package/dist/src/web/pages/status.js +446 -0
  88. package/dist/src/web/pages/syncs.js +155 -0
  89. package/dist/src/web/pages/tables.js +434 -0
  90. package/dist/src/web/pages/usage.js +397 -0
  91. package/dist/src/web/pages/users.js +218 -0
  92. package/dist/src/web/shared.js +568 -0
  93. package/dist/src/web/vendor/README.md +25 -0
  94. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  95. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  96. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  97. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  98. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  99. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  100. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  101. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  102. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  103. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  104. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  105. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  106. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  107. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  108. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  109. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  110. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  111. package/package.json +10 -3
@@ -1,76 +1,716 @@
1
- /* The web page of mason start (src/web/app.js). The CLI's palette (src/cli/style.ts): bold for a heading, the type
2
- colour for what you can type, dim for a description, green / red / yellow for a state only. No font is fetched:
3
- the system's, under the server's default-src 'self'. */
1
+ /* The web page of mason start (src/web/app.js; docs/WEB-UI.md "Presentation"). MASON'S OWN LOOK: a mason lays stone and
2
+ brick course by course, and Mason builds a copy of a Postgres table by table and keeps it standing. Warm graphite
3
+ words on a stone white (charcoal slate in the dark), mortar greys for every hairline, ONE patina (verdigris) for the
4
+ accent and the primary action; mono first (Geist Mono for the chrome, the labels, the numbers and the tables; Geist
5
+ Sans for a sentence of prose) under a slab display face (Zilla Slab) for the titles and the big numbers; square boxes
6
+ framed in a hairline, lifted by a quiet stone step; a copy's bar a thin solid line with its % at the end. Colour otherwise says a
7
+ state (ok / warn / bad / unknown), always beside its word. No gradient, no image: every texture is a border or a box.
8
+ Every font is Mason's own file (src/web/vendor/, served from the allowlist): nothing is fetched from elsewhere. */
9
+
10
+ @font-face {
11
+ font-family: "Geist";
12
+ src: url("vendor/geist-1.7.2/Geist-Variable.woff2") format("woff2");
13
+ font-weight: 100 900; font-style: normal; font-display: swap;
14
+ }
15
+ @font-face {
16
+ font-family: "Zilla Slab";
17
+ src: url("vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2") format("woff2");
18
+ font-weight: 700; font-style: normal; font-display: swap;
19
+ }
20
+ @font-face {
21
+ font-family: "Geist Mono";
22
+ src: url("vendor/geist-1.7.2/GeistMono-Variable.woff2") format("woff2");
23
+ font-weight: 100 900; font-style: normal; font-display: swap;
24
+ }
25
+
26
+ /* ---------------------------------------------------------------- tokens */
4
27
  :root {
5
- --bg: #fbfbfa; --fg: #1d1d1b; --dim: #6b6b66; --line: #e3e2de; --panel: #ffffff;
6
- --code: #0b6e8a; --ok: #1f7a3a; --bad: #b42318; --warn: #8a5a00; --bar: #0b6e8a;
7
- color-scheme: light dark;
28
+ /* stone white and warm graphite: the page, a box, a hover, a sunken strip, the sidebar's pale stone, the nav's current
29
+ row; the mortar inside a box, a control's edge, a box's hairline frame; the text in three steps */
30
+ --bg: #f6f5f2; --surface: #fdfcfb; --hover: #efede9; --sunken: #f2f0ec; --side: #f2f0ec; --current: #e9e6e0;
31
+ --line: #e8e5df; --line-strong: #cfcac1; --frame: #dcd8d0;
32
+ --fg: #2d2a27; --muted: #615c56; --subtle: #69645d;
33
+ /* the patina: the one accent (focus, a link's hover, the current nav row's edge, the brick being laid, a chart's mark),
34
+ verdigris on old stone; the states (each beside a word, never colour alone; the failed red a rose, never the accent) */
35
+ --accent: #2b6f70; --accent-soft: #e2eeed;
36
+ --ok: #2e7340; --warn: #865600; --bad: #a6344f; --unknown: #69645d;
37
+ --ok-soft: #e5f1e7; --warn-soft: #f7eedb; --bad-soft: #f9e5e8;
38
+ /* the primary action: patina, white words */
39
+ --cta: #2b6f70; --cta-hover: #235d5e; --cta-fg: #ffffff; --cta-line: #255f60;
40
+ /* a secondary button: a pale stone, a hairline lift */
41
+ --btn: #fdfcfb; --btn-hover: #f2f0ec;
42
+ --bevel: 0 1px 1px rgb(45 42 39 / 0.06);
43
+ --bevel-pressed: inset 0 1px 1px rgb(45 42 39 / 0.08);
44
+ /* the page's one shadow, quiet: a hairline stone step behind a box and a soft low lift (a hint of stacked stone) */
45
+ --stone-1: #e6e2db; --stone-2: #efece7; --shadow: var(--stone-1);
46
+ --drop: 1px 1px 0 var(--stone-1), 0 2px 6px -2px rgb(45 42 39 / 0.06); --drop-hover: 2px 2px 0 var(--stone-1), 0 8px 18px -8px rgb(45 42 39 / 0.14);
47
+ /* the sidebar's head and the phone's top bar: the sidebar's own pale stone; a table's header row and the raw log, a
48
+ tinted surface */
49
+ --chrome-bg: var(--side); --chrome-fg: var(--fg); --chrome-muted: var(--muted); --chrome-line: var(--line-strong);
50
+ --thead-bg: #f3f1ed; --thead-fg: #2d2a27; --thead-muted: #615c56; --zebra: #f9f8f6;
51
+ --code-bg: #f4f2ee; --code-fg: #36322e; --code-muted: #69645d;
52
+ /* the motifs' laid brick (the title's course, the checklist) patina; a copy's bar: its fill patina while it copies, its
53
+ track pale stone, served, failed and not kept current their own tints (validated together: dataviz's
54
+ validate_palette.js, --pairs all, the words beside) */
55
+ --brick: #199eaa; --glow: 0 0 0 transparent;
56
+ --bar-track: #e8e5df; --bar-ok: #33733f; --bar-warn: #c29647; --bar-bad: #a85071;
57
+ /* a chart (src/web/charts.js reads these): its trend in mortar grey, its mark and latest point the patina */
58
+ --chart-trend: #b8b2a8; --chart-mark: #2b6f70; --chart-grid: #ebe8e2; --chart-text: #69645d;
59
+ /* what dims the page under the phone's open menu */
60
+ --scrim: rgb(45 42 39 / 0.24);
61
+
62
+ --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
63
+ --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
64
+ --font-display: "Zilla Slab", "Rockwell", "Roboto Slab", Georgia, serif;
65
+ --text-xs: 12px; --text-sm: 13px; --text-md: 14px; --text-lg: 16px; --text-xl: 26px; --text-2xl: 34px;
66
+
67
+ --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 24px; --space-6: 32px; --space-7: 48px;
68
+ --radius-sm: 2px; --radius: 0; --radius-lg: 0;
69
+ --width: 1200px; --gutter: 24px; --topbar-h: 56px; --sidebar-w: 232px; --rail-w: 60px;
70
+ color-scheme: light;
8
71
  }
9
72
  @media (prefers-color-scheme: dark) {
10
- :root { --bg: #161615; --fg: #e8e7e3; --dim: #9a9993; --line: #2e2e2b; --panel: #1e1e1c;
11
- --code: #5cc3df; --ok: #5fcf80; --bad: #ff7b6e; --warn: #e9b64a; --bar: #5cc3df; }
73
+ :root {
74
+ /* charcoal slate, warm pale words, the patina lifted */
75
+ --bg: #16181c; --surface: #1c1f24; --hover: #252930; --sunken: #191c20; --side: #14161a; --current: #272b33;
76
+ --line: #2a2e35; --line-strong: #3f444d; --frame: #30353d;
77
+ --fg: #e7e5e1; --muted: #aaa69f; --subtle: #97938c;
78
+ --accent: #6cbcb8; --accent-soft: #1b3434;
79
+ --ok: #72c48f; --warn: #e2b45e; --bad: #ed8da4; --unknown: #97938c;
80
+ --ok-soft: #18291f; --warn-soft: #2c2517; --bad-soft: #351f26;
81
+ --cta: #5aaba7; --cta-hover: #6cbcb8; --cta-fg: #0d1919; --cta-line: #5aaba7;
82
+ --btn: #22262c; --btn-hover: #2a2f36;
83
+ --bevel: 0 1px 1px rgb(0 0 0 / 0.25);
84
+ --bevel-pressed: inset 0 1px 1px rgb(0 0 0 / 0.3);
85
+ --stone-1: #111316; --stone-2: #14161a;
86
+ --drop: 1px 1px 0 var(--stone-1), 0 2px 8px -2px rgb(0 0 0 / 0.3); --drop-hover: 2px 2px 0 var(--stone-1), 0 8px 18px -8px rgb(0 0 0 / 0.5);
87
+ --thead-bg: #20232a; --thead-fg: #e7e5e1; --thead-muted: #aaa69f; --zebra: #1f2227;
88
+ --code-bg: #121417; --code-fg: #dcdad5; --code-muted: #97938c;
89
+ --brick: #1ea2a8; --glow: 0 0 0 transparent;
90
+ --bar-track: #2c3038; --bar-ok: #2f743d; --bar-warn: #b58a3a; --bar-bad: #b35274;
91
+ --chart-trend: #4f555e; --chart-mark: #6cbcb8; --chart-grid: #2a2e35; --chart-text: #97938c;
92
+ --scrim: rgb(0 0 0 / 0.5);
93
+ color-scheme: dark;
94
+ }
12
95
  }
96
+
97
+ /* ---------------------------------------------------------------- base */
13
98
  * { box-sizing: border-box; }
14
- body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
15
- header, main, #poll { max-width: 1040px; margin: 0 auto; padding: 0 16px; }
16
- header { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 16px; padding-top: 24px; }
17
- h1 { font-size: 22px; margin: 0; }
18
- .where { margin: 0; color: var(--dim); }
19
- .updated { margin: 0 0 0 auto; color: var(--dim); font-size: 13px; }
20
- section { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 4px 20px 16px; margin: 16px 0; }
21
- h2 { font-size: 17px; margin: 14px 0 10px; }
22
- h3 { font-size: 15px; margin: 18px 0 6px; }
23
- code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; }
24
- code { color: var(--code); overflow-wrap: anywhere; }
25
- pre { white-space: pre-wrap; margin: 6px 0 0; color: var(--dim); }
26
- .dim, .lead { color: var(--dim); }
27
- .facts { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; margin: 0; }
28
- .facts dt { color: var(--dim); }
29
- .facts dd { margin: 0; overflow-wrap: anywhere; }
30
- table { width: 100%; border-collapse: collapse; }
31
- th, td { text-align: left; vertical-align: top; padding: 6px 8px 6px 0; border-top: 1px solid var(--line); font-weight: normal; }
32
- thead th { border-top: 0; color: var(--dim); font-size: 13px; }
33
- td.bar { white-space: nowrap; width: 1%; }
99
+ html { -webkit-text-size-adjust: 100%; }
100
+ body { margin: 0; background: var(--bg); color: var(--fg); font: var(--text-sm)/1.55 var(--font-mono); -webkit-font-smoothing: antialiased; }
101
+ /* a sentence of prose reads in the sans; everything else (the chrome, labels, numbers, tables) is mono */
102
+ .desc, .lead, .tip, .note, .empty p, .failure dd, .connect > p:not(.connect-title), .sort-note, .cut, .more p { font-family: var(--font-sans); font-size: var(--text-md); }
103
+ a { color: var(--fg); text-decoration: underline; text-decoration-color: var(--line-strong); text-underline-offset: 3px; text-decoration-thickness: 1px; }
104
+ a:hover { text-decoration-color: var(--accent); text-decoration-thickness: 2px; }
105
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 0; }
106
+ h1, h2, h3 { font-family: var(--font-mono); font-weight: 700; letter-spacing: -0.01em; }
107
+ /* the slab: a title and a big number, cut like a mason's letters (one weight, 700) */
108
+ h2, h3, .health, .card-value, .brand-name, .checklist-title, .connect.first .connect-title, .empty .empty-title { font-family: var(--font-display); font-weight: 700; letter-spacing: 0; }
109
+ h3 { font-size: 20px; margin: var(--space-6) 0 var(--space-3); }
110
+ code, pre, kbd, samp { font-family: var(--font-mono); font-size: 0.95em; }
111
+ code { overflow-wrap: anywhere; }
112
+ pre { white-space: pre-wrap; margin: var(--space-2) 0 0; color: var(--muted); }
113
+ .dim, .lead, .desc { color: var(--muted); }
114
+ .lead { margin: 0 0 var(--space-3); }
115
+ .mono, .num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
116
+ .count { margin: var(--space-3) 0 0; }
117
+ .warn { color: var(--warn); }
118
+
119
+ /* ---------------------------------------------------------------- the sidebar and the page beside it */
120
+ /* after Vercel's and Supabase's dashboards: a fixed column on the left (the project, the nav, when it was updated), the
121
+ page to its right; below 1000 px an icon rail, its labels tooltips; below 640 px a top bar whose menu opens the nav */
122
+ .sidebar { position: fixed; inset: 0 auto 0 0; width: var(--sidebar-w); display: flex; flex-direction: column; z-index: 20;
123
+ background: var(--side); border-right: 1px solid var(--frame); }
124
+ .content { margin-left: var(--sidebar-w); min-width: 0; }
125
+ main, #poll { max-width: var(--width); margin: 0 auto; padding-left: var(--gutter); padding-right: var(--gutter); }
126
+ /* the head: the mark on its patina tile, the project under it, a hairline below */
127
+ .side-head { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-4) var(--space-3) var(--space-3); flex: none;
128
+ background: var(--chrome-bg); color: var(--chrome-fg); border-bottom: 1px solid var(--line); }
129
+ .brand { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-lg); font-weight: 800; letter-spacing: -0.02em; margin: 0; padding: 0 var(--space-2); color: var(--chrome-fg); min-height: 24px; }
130
+ .brand-name { font-size: 22px; letter-spacing: 0.01em; line-height: 1; }
131
+ /* the mark: Mason's own arch of bricks, its keystone in the middle (index.html, an inline SVG in currentColor), on a brick tile */
132
+ .brand .logo { display: inline-flex; width: 24px; height: 24px; border-radius: 0; background: var(--cta); color: var(--cta-fg); border: 1px solid var(--cta-line);
133
+ box-shadow: 1px 1px 0 var(--stone-1); flex: none; }
134
+ .brand .logo svg { width: 100%; height: 100%; display: block; }
135
+ /* the project: its database and the demo badge, in a quiet box the height of its line before the first read (no shift) */
136
+ .project { display: flex; align-items: center; gap: var(--space-2); min-height: 36px; padding: var(--space-1) var(--space-2); min-width: 0;
137
+ border: 1px solid var(--chrome-line); border-radius: var(--radius); background: none; }
138
+ .where { margin: 0; font-size: var(--text-sm); font-weight: 500; color: var(--chrome-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1 1 auto; }
139
+ .where:empty::before { content: "…"; color: var(--chrome-muted); }
140
+ .side-body { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
141
+ /* the nav: full-width rows, the page shown a pale stone row with the patina at its edge */
142
+ .nav { flex: 1 1 auto; overflow-y: auto; padding: var(--space-4) 0; }
143
+ .nav ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0; }
144
+ .nav a { position: relative; display: flex; align-items: center; gap: var(--space-2); min-height: 30px; padding: 0 var(--space-4);
145
+ border-radius: 0; color: var(--fg); font-size: var(--text-sm); font-weight: 400; text-decoration: none; white-space: nowrap; }
146
+ .nav a:hover { color: var(--fg); background: var(--hover); text-decoration: none; }
147
+ .nav a[aria-current="page"] { color: var(--fg); background: var(--current); font-weight: 600; box-shadow: inset 3px 0 0 var(--accent); }
148
+ .nav a:focus-visible { outline-offset: -2px; border-radius: 0; }
149
+ .nav .group { margin-top: var(--space-4); }
150
+ .nav .group-title { margin: 0 0 var(--space-1); padding: 0 var(--space-4); font-size: var(--text-xs); font-weight: 700; color: var(--fg); text-transform: uppercase; letter-spacing: 0.06em; }
151
+ .nav .label { overflow: hidden; text-overflow: ellipsis; }
152
+ /* an icon: Lucide's 24-unit drawing at 16 px, its stroke a little lighter than its 2, in the link's colour */
153
+ .icon { display: inline-flex; flex: none; width: 16px; height: 16px; color: inherit; }
154
+ .icon svg { width: 16px; height: 16px; stroke-width: 1.75; }
155
+ .nav a .icon { color: var(--muted); }
156
+ .nav a:hover .icon { color: var(--fg); }
157
+ .nav a[aria-current="page"] .icon { color: var(--accent); }
158
+ /* the static nav's links have no icon until the script writes it: the same room kept, so nothing moves */
159
+ .nav a:not(:has(.icon))::before { content: ""; width: 16px; height: 16px; flex: none; }
160
+ .side-foot { flex: none; padding: var(--space-3) var(--space-4) var(--space-4); border-top: 1px solid var(--line-strong); font-size: var(--text-xs); color: var(--muted); }
161
+ .side-foot p { margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-height: 18px; }
162
+ .updated { font-variant-numeric: tabular-nums; }
163
+ .version { color: var(--subtle); font-family: var(--font-mono); }
164
+ .menu-button { display: none; }
165
+ .scrim { display: none; }
166
+ .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
167
+
168
+ /* a narrow window: an icon rail, each label the tooltip of its icon (still the link's name to a screen reader) */
169
+ @media (min-width: 641px) and (max-width: 999px) {
170
+ .sidebar { width: var(--rail-w); }
171
+ .content { margin-left: var(--rail-w); }
172
+ .side-head { align-items: center; padding: var(--space-4) 0 var(--space-3); }
173
+ .brand { padding: 0; }
174
+ .brand-name, .where { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
175
+ .project { border: 0; background: none; padding: 0; min-height: 20px; justify-content: center; }
176
+ .project .badge { padding: 0 4px; font-size: 10px; height: 18px; }
177
+ .nav { padding: var(--space-2) 0 var(--space-3); overflow: visible; }
178
+ .nav a { justify-content: center; padding: 0; min-height: 36px; }
179
+ .nav .label { position: absolute; left: calc(100% + var(--space-2)); top: 50%; transform: translateY(-50%); padding: var(--space-1) var(--space-2);
180
+ background: var(--fg); color: var(--surface); border-radius: 0; box-shadow: var(--drop); font-size: var(--text-xs); font-weight: 500; pointer-events: none; opacity: 0; z-index: 30; }
181
+ .nav a:hover .label, .nav a:focus-visible .label { opacity: 1; }
182
+ .nav .group { margin-top: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--line-strong); }
183
+ .nav .group-title { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
184
+ .side-foot { display: none; }
185
+ }
186
+
187
+ /* a phone: a top bar (the menu button, the mark, the project); the nav and the foot a sheet under it, opened by the button */
188
+ @media (max-width: 640px) {
189
+ .sidebar { position: sticky; inset: auto; top: 0; width: auto; height: var(--topbar-h); border-right: 0; border-bottom: 1px solid var(--frame); background: var(--chrome-bg); }
190
+ .content { margin-left: 0; }
191
+ .side-head { flex-direction: row; align-items: center; gap: var(--space-2); height: 100%; padding: 0 var(--space-4) 0 var(--space-2); border-bottom: 0; }
192
+ .brand { padding: 0; }
193
+ .project { flex: 1 1 auto; border: 0; background: none; padding: 0; min-height: 0; }
194
+ .where { flex: 0 1 auto; }
195
+ .menu-button { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 0; border: 0; background: none; box-shadow: none; color: var(--chrome-fg); flex: none; }
196
+ .menu-button:hover { background: var(--hover); }
197
+ .menu-button .icon, .menu-button .icon svg { width: 20px; height: 20px; }
198
+ .menu-button[aria-expanded="true"] .when-closed, .menu-button[aria-expanded="false"] .when-open { display: none; }
199
+ .side-body { display: none; position: fixed; top: var(--topbar-h); left: 0; bottom: 0; width: min(288px, 85vw); background: var(--side);
200
+ border-right: 1px solid var(--frame); box-shadow: var(--drop); z-index: 20; overflow-y: auto; }
201
+ .sidebar[data-open] .side-body { display: flex; }
202
+ .sidebar[data-open] + .scrim { display: block; position: fixed; inset: var(--topbar-h) 0 0 0; background: var(--scrim); z-index: 15; }
203
+ .nav a { min-height: 40px; }
204
+ }
205
+
206
+ /* ---------------------------------------------------------------- the page and its header */
207
+ main { padding-top: var(--space-6); padding-bottom: var(--space-7); }
208
+ section { margin: 0; }
209
+ .page-head h2 { font-size: var(--text-2xl); font-weight: 700; letter-spacing: -0.01em; line-height: 1.15; margin: 0 0 var(--space-2); }
210
+ /* under a page's title, a course being laid: three bricks set, two over them (one pseudo-element and its copies, no image) */
211
+ .page-head h2::after { content: ""; display: block; width: 14px; height: 5px; margin: var(--space-3) 0 2px; background: var(--brick);
212
+ box-shadow: 16px 0 0 var(--brick), 32px 0 0 var(--line-strong), 8px -7px 0 var(--brick), 24px -7px 0 var(--line), var(--glow); }
213
+ .page-head { margin: 0 0 var(--space-5); }
214
+ .page-head .desc { margin: 0; max-width: 78ch; }
215
+ #poll:empty { display: none; }
216
+ #poll { padding-top: var(--space-4); }
217
+
218
+ /* ---------------------------------------------------------------- the state: a dot and a word */
219
+ .indicator { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
220
+ /* a state's marker: a small stone block, a brick's proportions */
221
+ .indicator .dot { width: 10px; height: 6px; border-radius: 0; background: var(--unknown); flex: none; }
222
+ .indicator.ok .dot { background: var(--ok); } .indicator.warn .dot { background: var(--warn); }
223
+ .indicator.bad .dot { background: var(--bad); } .indicator.unknown .dot { background: var(--unknown); }
224
+ .indicator.warn .dot { animation: pulse 1.6s ease-in-out infinite; }
225
+ @keyframes pulse { 50% { opacity: 0.35; } }
226
+ @media (prefers-reduced-motion: reduce) { .indicator.warn .dot, .skeleton { animation: none; } }
227
+
228
+ /* the run's word at the top of the Overview: a big dot and the CLI's word */
229
+ .health { display: flex; align-items: center; gap: var(--space-3); margin: 0 0 var(--space-5); padding: var(--space-4) var(--space-5);
230
+ background: var(--surface); border: 1px solid var(--frame); border-radius: var(--radius-lg); box-shadow: var(--drop); font-size: var(--text-xl); font-weight: 800; letter-spacing: -0.02em; }
231
+ .health { font-size: 28px; font-weight: 700; letter-spacing: 0; }
232
+ /* its marker: a brick in the run's colour, set in a course of mortar of the same */
233
+ .health .dot { width: 22px; height: 12px; border-radius: 0; background: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft); flex: none; }
234
+ .health.ok .dot { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
235
+ .health.bad .dot { background: var(--bad); box-shadow: 0 0 0 3px var(--bad-soft); }
236
+
237
+ /* ---------------------------------------------------------------- badges and buttons */
238
+ /* a badge: a square tag, mono, its edge in its own colour */
239
+ .badge { display: inline-flex; align-items: center; height: 20px; padding: 0 6px; border-radius: 0; font-family: var(--font-mono); font-size: var(--text-xs); font-weight: 600;
240
+ background: var(--sunken); color: var(--muted); border: 1px solid var(--line-strong); white-space: nowrap; }
241
+ .badge.accent { background: var(--cta); color: var(--cta-fg); border-color: var(--cta-line); }
242
+ .badge.ok { background: var(--ok-soft); color: var(--ok); border-color: currentColor; }
243
+ .badge.warn { background: var(--warn-soft); color: var(--warn); border-color: currentColor; }
244
+ .badge.bad { background: var(--bad-soft); color: var(--bad); border-color: currentColor; }
245
+ /* a button: a pale stone key, a hairline lift, pressed in when clicked; the primary one patina */
246
+ button { font: inherit; font-family: var(--font-mono); font-size: var(--text-sm); font-weight: 500; height: 32px; padding: 0 var(--space-3); border-radius: var(--radius-sm);
247
+ border: 1px solid var(--line-strong); background: var(--btn); color: var(--fg); box-shadow: var(--bevel); cursor: pointer; white-space: nowrap; }
248
+ button:hover { background: var(--btn-hover); }
249
+ button:active:not(:disabled) { transform: translate(1px, 1px); box-shadow: var(--bevel-pressed); }
250
+ button.primary { background: var(--cta); color: var(--cta-fg); border-color: var(--cta-line); }
251
+ button.primary:hover { background: var(--cta-hover); }
252
+ button.danger { color: var(--bad); }
253
+ button.danger:hover { background: var(--bad-soft); border-color: var(--bad); }
254
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
255
+ button.small { height: 28px; padding: 0 var(--space-2); font-size: var(--text-xs); }
256
+ button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
257
+ button.primary:disabled, button:disabled:hover { opacity: 0.5; background: var(--btn); }
258
+ button.primary:disabled { background: var(--cta); }
259
+ /* the confirm of a destructive action (Remove orders, Cancel statement 3), in its popover: filled red, the one button that does it */
260
+ .pop-actions button.danger { background: var(--bad); color: var(--surface); border-color: var(--bad); }
261
+ .pop-actions button.danger:hover { opacity: 0.88; background: var(--bad); }
262
+
263
+ /* ---------------------------------------------------------------- form controls */
264
+ form { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin: var(--space-3) 0; }
265
+ label { font-size: var(--text-sm); color: var(--muted); }
266
+ input, select { font: inherit; font-family: var(--font-mono); font-size: var(--text-sm); height: 32px; padding: 0 var(--space-3); border-radius: 0;
267
+ border: 1px solid var(--line-strong); background: var(--surface); color: var(--fg); min-width: 14em; }
268
+ select { min-width: 0; }
269
+ input:hover, select:hover { border-color: var(--subtle); }
270
+ input:focus-visible, select:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
271
+ input::placeholder { color: var(--subtle); }
272
+ input[type="checkbox"] { min-width: 0; height: auto; accent-color: var(--accent); }
273
+ form#request-filters input { min-width: 8em; }
274
+ form.pick { display: block; margin: var(--space-1) 0 var(--space-2); }
275
+ form.pick fieldset { border: 1px solid var(--frame); border-radius: var(--radius); margin: 0 0 var(--space-2); padding: var(--space-2) var(--space-3); }
276
+ form.pick legend { color: var(--fg); font-size: var(--text-sm); font-weight: 700; padding: 0 var(--space-1); }
277
+ label.tick { display: inline-flex; align-items: baseline; gap: var(--space-1); margin: 2px 14px 2px 0; color: var(--fg); }
278
+ /* a tick's box is its own size: never the phone's input { flex: 1 1 10em }, which squeezed its column's name to a letter a line */
279
+ label.tick input { flex: none; }
280
+ label.tick code { overflow-wrap: normal; }
281
+
282
+ /* ---------------------------------------------------------------- KPI cards */
283
+ .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: var(--space-4); margin: 0 0 var(--space-6); }
284
+ /* by count, so no card is left alone on a row: one row on a wide screen; on a narrower one six as two rows of three,
285
+ five as three over two (a six-column grid, the last two spanning half each), four as two by two */
286
+ @media (min-width: 1100px) { .cards.n4 { grid-template-columns: repeat(4, 1fr); } .cards.n5 { grid-template-columns: repeat(5, 1fr); } .cards.n6 { grid-template-columns: repeat(3, 1fr); } }
287
+ /* six in one row only where each card is still wide enough for its sub-line (the sidebar takes 232 px of the width) */
288
+ @media (min-width: 1440px) { .cards.n6 { grid-template-columns: repeat(6, 1fr); } }
289
+ @media (min-width: 641px) and (max-width: 1099px) {
290
+ .cards.n4 { grid-template-columns: repeat(4, 1fr); }
291
+ .cards.n6 { grid-template-columns: repeat(3, 1fr); }
292
+ .cards.n5 { grid-template-columns: repeat(6, 1fr); } .cards.n5 > * { grid-column: span 2; } .cards.n5 > :nth-child(n+4) { grid-column: span 3; }
293
+ }
294
+ .card { background: var(--surface); border: 1px solid var(--frame); border-radius: var(--radius-lg); box-shadow: var(--drop); padding: var(--space-4); min-width: 0;
295
+ display: flex; flex-direction: column; gap: var(--space-1); }
296
+ .card-label { margin: 0; font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }
297
+ .card-value { margin: 0; font-size: var(--text-xl); line-height: 1.2; font-variant-numeric: lining-nums tabular-nums; overflow-wrap: anywhere; }
298
+ .card-sub { margin: 0; font-size: var(--text-xs); color: var(--muted); overflow-wrap: anywhere; }
299
+ .card.warn .card-value { color: var(--warn); } .card.bad .card-value { color: var(--bad); }
300
+ .card .chart { margin-top: auto; padding-top: var(--space-2); }
301
+
302
+ /* a chart: a progressive addition over numbers said as text; hidden (taking no room) until charts.js draws it */
303
+ .chart:not(.drawn) { display: none; }
304
+ .chart.spark { height: 48px; }
305
+ .chart.series { margin: var(--space-2) 0 var(--space-4); }
306
+ .chart .uplot { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--chart-text); }
307
+ .chart .u-legend { font-size: var(--text-xs); color: var(--muted); text-align: left; margin: var(--space-1) 0 0; }
308
+ .chart table.u-legend { width: auto; border: 0; border-radius: 0; background: none; font-size: var(--text-xs); }
309
+ .chart .u-legend th { font-weight: 500; }
310
+ .chart .u-legend tr:hover > *, .chart .u-legend td, .chart .u-legend th { background: none; border: 0; padding: 0 var(--space-2) 0 0; }
311
+
312
+ /* ---------------------------------------------------------------- tables: dense, hairline, mono numbers */
313
+ /* a box of rows: a hairline frame, its quiet lift, a tinted header row, every other row a shade */
314
+ .panel, .scroll { background: var(--surface); border: 1px solid var(--frame); border-radius: var(--radius-lg); box-shadow: var(--drop); margin: 0 0 var(--space-5); overflow-x: auto; }
315
+ table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: var(--text-sm); background: var(--surface);
316
+ border: 1px solid var(--frame); border-radius: var(--radius-lg); box-shadow: var(--drop); margin: 0 0 var(--space-5); }
317
+ .panel table, .scroll table { border: 0; margin: 0; border-radius: 0; box-shadow: none; }
318
+ caption { text-align: left; font-size: var(--text-md); font-weight: 700; padding: var(--space-5) 0 var(--space-2); color: var(--fg); }
319
+ caption code { font-weight: 400; color: var(--muted); }
320
+ th, td { text-align: left; vertical-align: top; padding: var(--space-2) var(--space-3); border-top: 1px solid var(--line); font-weight: 400; }
321
+ tbody tr:first-child > * { border-top: 0; }
322
+ /* a header row is a carved lintel: pale stone, its names cut in small capitals, a mortar course under it */
323
+ thead th { position: sticky; top: 0; background: var(--thead-bg); color: var(--thead-fg); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; border-top: 0;
324
+ border-bottom: 1px solid var(--line-strong); white-space: nowrap; padding-top: var(--space-2); padding-bottom: var(--space-2); }
325
+ thead th + th { border-left: 1px solid var(--line); }
326
+ tbody tr:nth-child(even) > * { background: var(--zebra); }
327
+ thead tr:first-child th:first-child { border-top-left-radius: var(--radius-lg); }
328
+ thead tr:first-child th:last-child { border-top-right-radius: var(--radius-lg); }
329
+ tbody tr:hover > * { background: var(--hover); }
330
+ tbody th[scope="row"] { color: var(--fg); font-weight: 500; white-space: nowrap; }
331
+ td.num, th.num, table.queries td, table.samples td.num, table.sizes td.num, table.catalog td.num { text-align: right; white-space: nowrap;
332
+ font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
34
333
  tbody th code { white-space: nowrap; overflow-wrap: normal; }
35
- progress { width: 120px; height: 8px; vertical-align: middle; accent-color: var(--bar); }
36
- .pct { display: inline-block; min-width: 3.2em; text-align: right; font-variant-numeric: tabular-nums; }
37
- .t-done progress { accent-color: var(--ok); }
38
- .t-failed progress, .t-failed .what { accent-color: var(--bad); color: var(--bad); }
39
- .t-unsynced progress, .t-unsynced .what { accent-color: var(--warn); color: var(--warn); }
40
- .count { margin: 10px 0 0; }
41
- .readiness { font-weight: 600; margin: 14px 0 4px; }
42
- .readiness.ok { color: var(--ok); }
43
- .readiness.bad { color: var(--bad); }
44
- .bullets { margin: 0; padding-left: 20px; }
45
- .warn { color: var(--warn); font-size: 13px; }
334
+ table.queries thead th:not(:first-child) { text-align: right; }
46
335
  td.act { text-align: right; white-space: nowrap; }
47
336
  .confirm { white-space: normal; }
48
- button { font: inherit; font-size: 14px; padding: 4px 12px; border-radius: 6px; border: 1px solid var(--line); background: var(--bg); color: var(--fg); cursor: pointer; }
49
- button:hover { border-color: var(--dim); }
50
- button.danger { border-color: var(--bad); color: var(--bad); }
51
- form { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 12px 0; }
52
- input { font: inherit; padding: 4px 8px; border-radius: 6px; border: 1px solid var(--line); background: var(--bg); color: var(--fg); min-width: 14em; }
53
- .failure { border-left: 3px solid var(--bad); padding: 8px 12px; margin: 12px 0; background: var(--panel); }
54
- .failure .message { margin: 0; font-weight: 600; }
337
+ tr.under > td { border-top: 0; padding-top: 0; }
338
+ .paging { margin: var(--space-3) 0 0; display: flex; gap: var(--space-2); }
339
+
340
+ /* the facts: a label column and a value column */
341
+ table.facts-table th[scope="row"] { width: 1%; padding-right: var(--space-5); }
342
+ table.facts-table td { overflow-wrap: anywhere; }
343
+ /* the recent errors: a client's name, a user's or a statement unbroken by spaces wraps inside its cell, never past the page */
344
+ table.errors td { overflow-wrap: anywhere; }
345
+ .facts { display: grid; grid-template-columns: max-content 1fr; gap: var(--space-1) var(--space-4); margin: 0; font-size: var(--text-sm); }
346
+ .facts dt { color: var(--muted); }
347
+ .facts dd { margin: 0; overflow-wrap: anywhere; font-family: var(--font-mono); }
348
+ tr.warn th, tr.warn td { color: var(--warn); }
349
+
350
+ /* A COPY'S BAR (copyBar, src/web/pages/status.js): a thin rounded track and a solid fill to the share, its % at the end.
351
+ The fill's colour follows the copy's state as the line's word does: the patina while it copies, green once served,
352
+ rose when it failed, amber when not kept current; waiting, no fill, the track's quiet shimmer (not under
353
+ prefers-reduced-motion). The share drawn never goes backwards (createMonotone). */
354
+ td.bar { white-space: nowrap; width: 1%; }
355
+ .copybar { display: inline-flex; align-items: center; vertical-align: middle; width: 120px; }
356
+ .copybar .track { display: block; position: relative; width: 100%; height: 6px; border-radius: 3px; background: var(--bar-track); overflow: hidden; }
357
+ .copybar .fill { display: block; height: 100%; border-radius: 3px; background: var(--brick); }
358
+ .t-done .copybar .fill { background: var(--bar-ok); }
359
+ .t-failed .copybar .fill { background: var(--bar-bad); }
360
+ .t-unsynced .copybar .fill { background: var(--bar-warn); }
361
+ .copybar.indeterminate .track { animation: bar-wait 1.6s ease-in-out infinite; }
362
+ @keyframes bar-wait { 50% { opacity: 0.45; } }
363
+ @media (prefers-reduced-motion: reduce) { .copybar.indeterminate .track { animation: none; } }
364
+ .pct { display: inline-block; min-width: 3.2em; text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--muted); }
365
+ .t-failed .what, .s-failed .what { color: var(--bad); }
366
+ .t-unsynced .what { color: var(--warn); }
367
+ .readiness { font-weight: 700; margin: var(--space-4) 0 var(--space-1); }
368
+
369
+ /* ---------------------------------------------------------------- details: an expandable row */
370
+ details > summary { cursor: pointer; color: var(--muted); font-size: var(--text-sm); list-style: none; display: inline-flex; align-items: center; gap: 6px; user-select: none; }
371
+ details > summary::-webkit-details-marker { display: none; }
372
+ details > summary::before { content: "+" / ""; display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px;
373
+ border: 1px solid currentColor; font-size: 11px; line-height: 1; font-weight: 700; margin-right: 2px; }
374
+ details[open] > summary::before { content: "−" / ""; }
375
+ details > summary:hover { color: var(--fg); }
376
+ details[open] { margin-bottom: var(--space-2); }
377
+ details pre { white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--text-xs); margin: var(--space-1) 0; }
378
+ details.memory, details#syncs-raw { margin: var(--space-4) 0; }
379
+
380
+ /* ---------------------------------------------------------------- what the commands say: a Failure, an outcome */
381
+ .failure { border: 1px solid var(--frame); border-left: 4px solid var(--bad); border-radius: var(--radius); box-shadow: var(--drop); padding: var(--space-3) var(--space-4);
382
+ margin: var(--space-3) 0 var(--space-4); background: var(--surface); }
383
+ .failure .message { margin: 0; font-weight: 700; }
55
384
  .failure .mark { color: var(--bad); }
56
- .failure dl { display: grid; grid-template-columns: max-content 1fr; gap: 2px 12px; margin: 6px 0 0; }
57
- .failure dt { color: var(--dim); }
385
+ .failure dl { display: grid; grid-template-columns: max-content 1fr; gap: 2px var(--space-3); margin: var(--space-2) 0 0; font-size: var(--text-sm); }
386
+ .failure dt { color: var(--muted); }
58
387
  .failure dd { margin: 0; white-space: pre-line; overflow-wrap: anywhere; }
59
- .next-head { margin: 8px 0 2px; font-weight: 600; }
60
- .next { list-style: none; margin: 0; padding: 0; }
61
- .next li span { color: var(--dim); margin-left: 8px; }
62
- .added { border: 1px solid var(--ok); border-radius: 8px; padding: 10px 14px; margin: 12px 0; }
63
- .added .headline { font-weight: 600; margin: 0 0 8px; }
64
- .added .tip { color: var(--dim); }
65
- #poll .failure { border: 1px solid var(--bad); border-left-width: 3px; border-radius: 4px; margin-top: 16px; }
66
- table.catalog td.num { white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; }
67
- .c-added .state { color: var(--ok); }
68
- .c-cannot .state { color: var(--warn); font-size: 13px; }
69
- tr.under > td { border-top: 0; padding-top: 0; }
70
- form.pick { display: block; margin: 4px 0 8px; }
71
- form.pick fieldset { border: 1px solid var(--line); border-radius: 6px; margin: 0 0 8px; padding: 6px 10px; }
72
- form.pick legend { color: var(--dim); font-size: 13px; padding: 0 4px; }
73
- label.tick { display: inline-flex; align-items: baseline; gap: 4px; margin: 2px 14px 2px 0; }
74
- .done { border: 1px solid var(--ok); border-radius: 8px; padding: 8px 12px; margin: 4px 0 8px; }
75
- .done .headline { font-weight: 600; margin: 0 0 4px; overflow-wrap: anywhere; }
76
- .done .note, .done .tip { color: var(--dim); margin: 2px 0 6px; }
388
+ .next-head { margin: var(--space-2) 0 2px; font-weight: 700; font-size: var(--text-sm); }
389
+ .next { list-style: none; margin: 0; padding: 0; font-size: var(--text-sm); }
390
+ .next li span { color: var(--muted); margin-left: var(--space-2); }
391
+ .added, .done { border: 1px solid var(--frame); border-left: 4px solid var(--ok); border-radius: var(--radius); box-shadow: var(--drop); padding: var(--space-3) var(--space-4); margin: var(--space-3) 0 var(--space-4); background: var(--surface); }
392
+ .added .headline, .done .headline { font-weight: 700; margin: 0 0 var(--space-2); overflow-wrap: anywhere; }
393
+ .added .tip, .done .note, .done .tip { color: var(--muted); margin: 2px 0 6px; }
394
+
395
+ /* ---------------------------------------------------------------- empty and loading */
396
+ .empty { text-align: center; padding: var(--space-7) var(--space-4); color: var(--muted); background: var(--surface);
397
+ border: 1px dashed var(--frame); border-radius: var(--radius-lg); margin: 0 0 var(--space-5); }
398
+ /* over an empty state, a footing with nothing built on it yet: three stones laid, the courses above them only marked */
399
+ .empty::before { content: ""; display: block; width: 18px; height: 7px; margin: 0 auto var(--space-4); transform: translateX(-20px); background: var(--line-strong);
400
+ box-shadow: 20px 0 0 var(--line-strong), 40px 0 0 var(--line-strong), 10px -9px 0 var(--line), 30px -9px 0 var(--line), 20px -18px 0 var(--line); margin-top: 18px; }
401
+ .empty .empty-title { margin: 0 0 var(--space-1); color: var(--fg); font-size: 20px; }
402
+ .empty p { margin: 0; }
403
+ .skeleton { display: block; height: 14px; border-radius: 0; background: var(--sunken); animation: shimmer 1.4s ease-in-out infinite; }
404
+ .skeleton + .skeleton { margin-top: var(--space-2); }
405
+ .skeleton.half { width: 50%; } .skeleton.block { height: 96px; border-radius: var(--radius-lg); }
406
+ @keyframes shimmer { 50% { opacity: 0.5; } }
407
+
408
+ /* ---------------------------------------------------------------- a phone */
409
+ @media (max-width: 640px) {
410
+ :root { --gutter: 16px; }
411
+ main { padding-top: var(--space-5); }
412
+ .cards { grid-template-columns: 1fr 1fr; gap: var(--space-2); }
413
+ .card { padding: var(--space-3); }
414
+ .card-value { font-size: var(--text-lg); }
415
+ .health { font-size: var(--text-lg); padding: var(--space-3) var(--space-4); }
416
+ /* a table scrolls inside its card, never the page (its caption then heads the card) */
417
+ table { display: block; overflow-x: auto; }
418
+ caption { display: block; padding: var(--space-3) var(--space-3) var(--space-1); }
419
+ input { min-width: 0; flex: 1 1 10em; }
420
+ }
421
+
422
+ /* ---------------------------------------------------------------- the Tables and Users pages */
423
+ .toolbar input[type="search"] { width: 18em; }
424
+ #page-tables th.num, #page-users th.num { text-align: right; }
425
+ #page-tables td.mono { white-space: nowrap; color: var(--muted); }
426
+ .err { color: var(--bad); overflow-wrap: anywhere; }
427
+ #page-tables .done button { margin-top: var(--space-1); }
428
+ /* THE TABLES LIST FITS ITS CARD AND NOTHING A CLICK DOES MOVES IT (src/web/pages/tables.js renderTables): seven short
429
+ cells a row at FIXED widths (`table-layout: fixed`: a column is as wide as its header says, whatever a row's cells
430
+ hold now), the name the column that gives (cut with an ellipsis, its whole the title); every row two lines (its state
431
+ over its bar, a line kept when it has none) and its note under it, across every column, written for every row (empty
432
+ when it says nothing). A question, a form and an answer are popovers beside the row's actions (`.pop`), never a row.
433
+ The row's actions never wrap and never leave the card: they stay pinned at its right where the table scrolls. */
434
+ table.catalog { table-layout: fixed; width: 100%; min-width: 840px; }
435
+ table.catalog th.state { width: 146px; }
436
+ table.catalog th.rows { width: 116px; }
437
+ table.catalog th.key { width: 104px; }
438
+ table.catalog th.last { width: 104px; }
439
+ table.catalog th.size { width: 100px; }
440
+ table.catalog th.act { width: 268px; }
441
+ table.catalog th, table.catalog td { overflow: hidden; text-overflow: ellipsis; }
442
+ table.catalog code.name { display: block; max-width: 100%; }
443
+ table.catalog td.state { white-space: nowrap; }
444
+ table.catalog td.state .copyline { display: flex; flex-wrap: nowrap; align-items: center; height: 18px; margin-top: var(--space-1); }
445
+ table.catalog td.state .pct { min-width: 0; }
446
+ table.catalog td.state .copybar { width: 64px; }
447
+ table.catalog td.num .took { display: block; color: var(--muted); font-size: var(--text-xs); }
448
+ table.catalog tbody tr:not(.under) > * { padding-bottom: var(--space-1); }
449
+ table.catalog tr.under > td { padding-top: 0; }
450
+ table.catalog tr.note .under-body { font-size: var(--text-sm); line-height: 20px; display: flex; flex-wrap: wrap; gap: 2px var(--space-3); min-height: 20px; }
451
+ .under-body { min-width: 0; overflow-wrap: anywhere; }
452
+ /* A POPOVER (`pop`, src/web/shared.js): fixed beside the button that asked, over the rows, never between them; placed by
453
+ placePops after each write. A question or a form scrolls in itself when long; an answer goes by itself. */
454
+ .pop { position: fixed; top: 0; left: 0; z-index: 40; width: max-content; max-width: min(30rem, calc(100vw - 32px)); padding: var(--space-3) var(--space-4);
455
+ background: var(--surface); border: 1px solid var(--frame); border-radius: var(--radius-lg); box-shadow: var(--drop-hover); font-size: var(--text-sm); overflow-wrap: anywhere; }
456
+ .pop[hidden] { display: none; }
457
+ .pop.ask { max-height: min(70vh, 34rem); overflow-y: auto; border-top: 3px solid var(--accent); }
458
+ .pop.ask:has(.confirm) { border-top-color: var(--bad); }
459
+ .pop.said { border-left: 4px solid var(--ok); }
460
+ .pop.said:has(.failure) { border-left-color: var(--bad); }
461
+ .pop > :first-child { margin-top: 0; }
462
+ .pop > :last-child { margin-bottom: 0; }
463
+ .pop .confirm { margin: 0 0 var(--space-2); color: var(--fg); }
464
+ .pop .pop-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0; }
465
+ .pop .done { border: 0; box-shadow: none; padding: 0; margin: 0; background: none; }
466
+ /* a confirm's button names what it removes: a long name is cut with an ellipsis, never past the popover (its name is still its whole text) */
467
+ .pop button { max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; }
468
+ /* A BUTTON WHOSE LABEL CHANGES KEEPS ITS WIDTH (`room`, src/web/shared.js ROOMS): its longest label laid under it, unseen
469
+ and of no height, so Sync, Syncing… and Synced ✓ (Copy, Copied) are one width */
470
+ button[data-room]::after { content: attr(data-room); display: block; height: 0; overflow: hidden; visibility: hidden; }
471
+ td.act, th.act { white-space: nowrap; text-align: right; }
472
+ td.act > * { vertical-align: middle; }
473
+ table.catalog td.act, table.catalog th.act { position: sticky; right: 0; }
474
+ @media (max-width: 640px) { table.catalog td.act, .panel td.act, .scroll td.act { box-shadow: -1px 0 0 var(--line); } }
475
+ table.catalog td.act { background: var(--surface); }
476
+ table.catalog tbody tr:hover > td.act { background: var(--hover); }
477
+ .what-copy.t-unsynced { color: var(--warn); }
478
+ .what-copy.t-copying, .what-copy.t-waiting { color: var(--muted); }
479
+ /* a narrower window (the sidebar beside the page): every table's cells closer, so more fit their card */
480
+ @media (max-width: 1100px) { th, td { padding-left: var(--space-2); padding-right: var(--space-2); } }
481
+ /* a bare table too wide for the page (the Overview's statements: eleven numbers) scrolls inside its own box */
482
+ .table-wrap { max-width: 100%; overflow-x: auto; margin: 0 0 var(--space-4); }
483
+ .table-wrap > table { margin: 0; }
484
+ /* a phone, where a table scrolls inside its card: a row's actions stay pinned at its right, in view */
485
+ @media (max-width: 640px) {
486
+ .panel td.act, .scroll td.act { position: sticky; right: 0; background: var(--surface); }
487
+ .panel tbody tr:hover > td.act, .scroll tbody tr:hover > td.act { background: var(--hover); }
488
+ }
489
+ /* a phone: the table scrolls inside its card, so a line under a row stays in the card's view, never as wide as the table */
490
+ @media (max-width: 640px) {
491
+ /* the table scrolls inside its card (.panel) at its least width (table.catalog's min-width), still a table — a block
492
+ would lose its fixed columns — the name still the column that gives */
493
+ table.catalog { display: table; min-width: 940px; }
494
+ }
495
+ @media (max-width: 640px) { tr.under .under-body { position: sticky; left: var(--space-3); max-width: calc(100vw - 2 * var(--gutter) - 2 * var(--space-3)); } }
496
+ @media (max-width: 640px) { table.catalog tr.under > td { overflow: visible; } }
497
+ tr.removed > * { color: var(--muted); }
498
+ form.formcard { margin: 0 0 var(--space-5); padding: var(--space-3) var(--space-4); background: var(--surface); border: 1px solid var(--frame); border-radius: var(--radius-lg); box-shadow: var(--drop); }
499
+ form.formcard input { width: 18em; }
500
+ /* ---------------------------------------------------------------- Connect: the URL, its psql line, Create a user */
501
+ /* a box whose title sits on its top edge, as a fieldset's legend does (Connect, Getting started, a chart's name) */
502
+ .connect { background: var(--surface); border: 1px solid var(--frame); border-radius: var(--radius-lg); box-shadow: var(--drop); padding: var(--space-4) var(--space-5); margin: var(--space-6) 0 var(--space-6); }
503
+ .connect.first { border-width: 2px; }
504
+ .connect .connect-title, .checklist .checklist-title { width: fit-content; max-width: 100%; margin: calc(-1 * var(--space-4) - 0.8em) 0 var(--space-2) calc(-1 * var(--space-2));
505
+ padding: 0 var(--space-2); background: var(--surface); font-weight: 700; line-height: 1.6; }
506
+ .connect.first .connect-title { font-size: var(--text-lg); }
507
+ .connect .facts { margin: 0 0 var(--space-3); gap: var(--space-2) var(--space-4); align-items: center; }
508
+ .connect .copyrow { flex-wrap: wrap; }
509
+ .connect .copyrow code { overflow-wrap: anywhere; }
510
+ .connect .facts dd .dim { font-family: var(--font-sans); font-size: var(--text-sm); }
511
+ .connect .tip { margin: var(--space-3) 0 0; color: var(--muted); display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); }
512
+ .empty + .connect { margin-top: var(--space-3); }
513
+ /* the first-run checklist (renderChecklist): a step a row, ✓ in green or ○ muted, a step to do with its one action */
514
+ .checklist { background: var(--surface); border: 2px solid var(--frame); border-radius: var(--radius-lg); box-shadow: var(--drop); padding: var(--space-4) var(--space-5); margin: var(--space-6) 0 var(--space-6); }
515
+ .checklist .checklist-title { margin-bottom: var(--space-3); font-size: var(--text-lg); display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-3); }
516
+ .checklist .checklist-title .dim { font-weight: 400; font-size: var(--text-sm); font-family: var(--font-mono); }
517
+ /* the steps as courses of a wall, built from the bottom: a step done is a brick laid, a step to do its place marked */
518
+ .checklist { position: relative; }
519
+ .checklist ol { padding-right: 64px; }
520
+ .checklist .courses { position: absolute; right: var(--space-5); top: var(--space-4); display: flex; flex-direction: column-reverse; gap: 3px; }
521
+ .checklist .courses i { display: block; width: 26px; height: 9px; border: 1px dashed var(--line-strong); }
522
+ .checklist .courses i:nth-child(even) { margin-left: 13px; }
523
+ .checklist .courses i.on { background: var(--brick); border-color: var(--brick); box-shadow: var(--glow); }
524
+ .checklist ol { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
525
+ .checklist li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); min-height: 32px; }
526
+ /* a step's mark: a small stone, filled in green once done, its outline while to do */
527
+ .checklist .mark { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 14px; font-size: 10px; font-weight: 700; line-height: 1;
528
+ border: 1px solid currentColor; }
529
+ .checklist li.step-done .mark { background: var(--ok); border-color: var(--ok); color: var(--surface); }
530
+ .checklist li.step-done .what { color: var(--muted); }
531
+ .checklist li.step-todo .mark { color: var(--subtle); border-style: dashed; }
532
+ .checklist li.step-todo .what { font-weight: 600; }
533
+ /* a link that looks like a button (it goes to another page: a link, for the browser and a screen reader alike) */
534
+ a.btn { display: inline-flex; align-items: center; height: 28px; padding: 0 var(--space-2); border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: 500;
535
+ border: 1px solid var(--line-strong); background: var(--btn); color: var(--fg); box-shadow: var(--bevel); white-space: nowrap; text-decoration: none; }
536
+ a.btn:hover { background: var(--btn-hover); text-decoration: none; }
537
+ a.btn:active { transform: translate(1px, 1px); box-shadow: var(--bevel-pressed); }
538
+ a.btn.primary { background: var(--cta); color: var(--cta-fg); border-color: var(--cta-line); }
539
+ a.btn.primary:hover { background: var(--cta-hover); }
540
+ a.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
541
+ .copyrow { display: inline-flex; align-items: center; gap: var(--space-2); max-width: 100%; }
542
+ .copyrow code { padding: 2px var(--space-2); background: var(--sunken); border: 1px solid var(--line-strong); border-radius: 0; }
543
+ /* a URL in a table's cell: one line, cut with an ellipsis, its whole the title (Copy copies the whole) */
544
+ .copyrow button { flex: none; }
545
+ .copyrow button[data-copied] { color: var(--ok); border-color: var(--ok); }
546
+ /* the Users table fits its card at a laptop's width: a user's id under its name, the URL cut shorter */
547
+ table.users th[scope="row"] .dim { display: block; font-size: var(--text-xs); font-weight: 400; }
548
+ /* a long user name (40 characters) is cut with its whole the title; a user that no longer connects says why on lines of its own */
549
+ table.users code.name { max-width: 24ch; }
550
+ table.users td .warn { display: block; white-space: normal; min-width: 14em; }
551
+ @media (max-width: 1100px) { table.users code.name { max-width: 18ch; } }
552
+ table.users td { white-space: nowrap; }
553
+ /* FIXED COLUMNS, as the tables' catalog's: Disable → "Disabled <when>" widens no column and moves no button */
554
+ table.users { table-layout: fixed; width: 100%; min-width: 960px; }
555
+ table.users th.status { width: 224px; }
556
+ table.users th.added { width: 188px; }
557
+ table.users th.last { width: 150px; }
558
+ table.users th.act { width: 240px; }
559
+ table.users td { overflow: hidden; text-overflow: ellipsis; }
560
+ #page-users td.mono { white-space: nowrap; }
561
+ #page-users .added { border-left-color: var(--warn); border-radius: var(--radius-lg); padding: var(--space-4) var(--space-5); margin: 0 0 var(--space-4); }
562
+ #page-users .added .headline { font-size: var(--text-lg); display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
563
+ #page-users .added .facts { margin: 0 0 var(--space-3); gap: var(--space-2) var(--space-4); align-items: center; }
564
+ #page-users .added .tip { margin: 0 0 var(--space-3); }
565
+ /* a phone: a fact's label over its value, so a URL or a password has the card's whole width */
566
+ @media (max-width: 640px) { .added .facts, .connect .facts { grid-template-columns: 1fr; gap: 0 var(--space-4); } .added .facts dd, .connect .facts dd { margin-bottom: var(--space-2); } }
567
+
568
+ /* ---------------------------------------------------------------- Queries, Usage and Logs (scoped to those pages) */
569
+ /* a filter toolbar: each field a small label over its control, the actions at the end */
570
+ .toolbar { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-3); margin: 0 0 var(--space-5); padding: var(--space-3) var(--space-4);
571
+ background: var(--sunken); border: 1px solid var(--frame); border-radius: var(--radius-lg); }
572
+ .toolbar label { font-size: var(--text-xs); font-weight: 600; color: var(--fg); }
573
+ .toolbar .field { display: flex; flex-direction: column; gap: var(--space-1); }
574
+ .toolbar .actions { display: flex; gap: var(--space-2); margin-left: auto; }
575
+ .toolbar label.check { display: inline-flex; align-items: center; gap: var(--space-2); min-height: 32px; color: var(--fg); font-size: var(--text-sm); }
576
+ .toolbar input[type="checkbox"] { margin: 0; }
577
+ #page-queries .table-title { margin: var(--space-3) 0 var(--space-2); font-size: var(--text-sm); font-weight: 600; color: var(--muted); }
578
+ #page-queries .paging { justify-content: flex-end; margin: 0 0 var(--space-4); }
579
+ #page-queries table.requests td.id { white-space: nowrap; }
580
+ #page-queries table.requests td.id summary { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--fg); }
581
+ #page-queries table.requests td.id details { margin: 0; }
582
+ #page-queries table.requests td.when { white-space: nowrap; color: var(--muted); }
583
+ #page-queries .err { margin-top: 2px; font-size: var(--text-xs); color: var(--bad); overflow-wrap: anywhere; }
584
+ #page-queries tr.detail > td { background: var(--sunken); }
585
+ /* an expanded row pushes the rows under it down (the reader asked) but never widens a column: its body takes the row's
586
+ width and adds none of its own (width 0, at least the row's), a long statement scrolling in its box */
587
+ #page-queries tr.detail .detail-body { width: 0; min-width: 100%; overflow-wrap: anywhere; }
588
+ #page-queries tr.detail:hover > td { background: var(--sunken); }
589
+ #page-queries tr.detail pre.stmt { font-family: var(--font-mono); color: var(--fg); }
590
+ #page-queries tr.detail pre.error { color: var(--bad); }
591
+ #page-queries table.sessions td:first-child { font-weight: 600; }
592
+
593
+ /* Usage: four small charts in a grid, each under its title; a card with no drawn chart takes no room */
594
+ .chart-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(420px, 100%), 1fr)); gap: var(--space-6) var(--space-4); margin: var(--space-6) 0 var(--space-6); }
595
+ .chart-card { background: var(--surface); border: 1px solid var(--frame); border-radius: var(--radius-lg); box-shadow: var(--drop); padding: var(--space-4); min-width: 0; }
596
+ /* a chart's title INSIDE its card, over its chart (never on the card's edge: a title set into the frame floated out of
597
+ it), and the chart as wide as the card's content (charts.js sizes uPlot from its holder's clientWidth, again whenever
598
+ the holder's width changes): nothing of a chart card leaves it, at any width */
599
+ .chart-card > .card-label:first-child { max-width: 100%; margin: 0 0 var(--space-2); color: var(--fg); font-size: var(--text-sm); font-weight: 700; line-height: 1.6; }
600
+ .chart-card .chart { min-width: 0; max-width: 100%; }
601
+ .chart-card:not(:has(.chart.drawn)) { display: none; }
602
+ .chart-card .chart.series { margin: var(--space-2) 0 0; }
603
+ details.hours { margin: var(--space-3) 0; }
604
+ details.hours > summary { margin-bottom: var(--space-2); }
605
+ @media (max-width: 640px) { .chart-grid { grid-template-columns: 1fr; } }
606
+
607
+ /* Logs: the run's raw log as a terminal's lines, in mono, the newest at the bottom */
608
+ #page-logs table.events th[scope="row"] time { font-weight: 400; }
609
+ #page-logs table.events code { color: var(--fg); }
610
+ .terminal { background: var(--code-bg); color: var(--code-fg); border: 1px solid var(--frame); border-radius: var(--radius-lg); box-shadow: var(--drop); padding: var(--space-3) var(--space-4);
611
+ max-height: 480px; overflow: auto; font-family: var(--font-mono); font-size: var(--text-xs); line-height: 1.6; }
612
+ .terminal .log-line { display: grid; grid-template-columns: max-content 1fr; gap: var(--space-3); }
613
+ .terminal time { color: var(--code-muted); white-space: nowrap; }
614
+ .terminal pre { margin: 0; color: var(--code-fg); font-size: inherit; overflow-wrap: anywhere; }
615
+ #page-queries table.running td:nth-child(5) { white-space: nowrap; }
616
+
617
+ /* A chart's live legend reads the hovered hour: hidden at rest, where it would only say "-" (its space kept, so nothing moves). */
618
+ .uplot:not(:hover) .u-legend { visibility: hidden; }
619
+
620
+ /* ---------------------------------------------------------------- an Observability view's head: its title left, the time range right, one row */
621
+ .head-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4); margin: 0 0 var(--space-2); }
622
+ .head-row h2 { margin: 0; }
623
+ .head-range:empty { display: none; }
624
+ .head-range { min-width: 0; max-width: 100%; display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; justify-content: flex-end; }
625
+ /* beside the range: Now's "Live" (a dot that breathes), or when a past range was read and its Refresh */
626
+ .head-range .live { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: var(--text-xs); font-weight: 500; color: var(--ok); }
627
+ .live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 0 var(--ok); animation: live 2s ease-out infinite; }
628
+ @keyframes live { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ok) 50%, transparent); } 100% { box-shadow: 0 0 0 6px transparent; } }
629
+ @media (prefers-reduced-motion: reduce) { .live-dot { animation: none; } }
630
+ .head-range .as-of { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-xs); white-space: nowrap; }
631
+ /* the time range: square segments in one hairline frame, the one chosen a pale stone with a patina underline */
632
+ .range { display: inline-flex; padding: 0; margin: 0; background: var(--surface); border: 1px solid var(--frame); border-radius: var(--radius); box-shadow: var(--bevel); }
633
+ .range a { display: inline-flex; align-items: center; min-height: 30px; padding: 0 var(--space-3); border-radius: 0; color: var(--fg);
634
+ font-size: var(--text-xs); font-weight: 500; white-space: nowrap; text-decoration: none; }
635
+ .range > * + * { border-left: 1px solid var(--frame); }
636
+ .range a:hover { background: var(--hover); text-decoration: none; }
637
+ .range a[aria-current="true"] { background: var(--current); color: var(--fg); font-weight: 700; box-shadow: inset 0 -2px 0 var(--accent); }
638
+ /* a range past what the run keeps: shown, never a link, its title why and the setting that keeps it */
639
+ .range .off { display: inline-flex; align-items: center; min-height: 30px; padding: 0 var(--space-3); color: var(--subtle); font-size: var(--text-xs); font-weight: 500;
640
+ white-space: nowrap; cursor: not-allowed; opacity: 0.6; }
641
+ /* five ranges on a phone: the control scrolls inside itself, never the page */
642
+ .range { max-width: 100%; overflow-x: auto; }
643
+
644
+ /* ---------------------------------------------------------------- a card that links into the view that lists it */
645
+ a.card { color: inherit; text-decoration: none; transition: transform 0.08s, box-shadow 0.08s; }
646
+ a.card:hover { transform: translate(-2px, -2px); box-shadow: var(--drop-hover); text-decoration: none; background: var(--surface); }
647
+ @media (prefers-reduced-motion: reduce) { a.card { transition: none; } }
648
+ a.card .card-label { display: flex; justify-content: space-between; gap: var(--space-2); }
649
+ a.card .card-label::after { content: "→" / ""; color: var(--subtle); transition: transform 0.12s, color 0.12s; }
650
+ a.card:hover .card-label::after { color: var(--accent); transform: translateX(2px); }
651
+ caption a.see { font-weight: 400; font-size: var(--text-sm); margin-left: var(--space-2); }
652
+
653
+ /* ---------------------------------------------------------------- sortable headers, long values, large lists */
654
+ th[aria-sort] { padding-top: 3px; padding-bottom: 3px; }
655
+ button.sort { all: unset; box-shadow: none; display: inline-flex; align-items: center; gap: 4px; min-height: 28px; cursor: pointer; color: inherit; font: inherit; white-space: nowrap; }
656
+ button.sort:hover, button.sort.on { color: var(--fg); }
657
+ thead button.sort:hover, thead button.sort.on { color: var(--thead-fg); text-decoration: underline; text-underline-offset: 3px; }
658
+ thead button.sort .arrow { color: var(--thead-muted); }
659
+ thead button.sort.on .arrow { color: var(--thead-fg); }
660
+ button.sort:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 0; }
661
+ button.sort .arrow { font-size: 10px; color: var(--subtle); }
662
+ button.sort.on .arrow { color: var(--fg); }
663
+ th.num button.sort { flex-direction: row-reverse; }
664
+ code.name { display: inline-block; max-width: 36ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
665
+ .clip { display: inline-block; max-width: 28em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
666
+ /* a statement in its cell: one or two lines, never the whole of it (its expanded row has it, capped) */
667
+ code.stmt { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
668
+ line-clamp: 2; white-space: normal; overflow: hidden; text-overflow: ellipsis; overflow-wrap: anywhere; max-width: 40em; vertical-align: bottom; }
669
+ #page-queries table.running code.stmt { max-width: 24em; }
670
+ #page-queries table.sessions .clip { max-width: 14em; }
671
+ #page-queries table.running .clip { max-width: 9em; }
672
+ .more { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--space-2); margin: calc(-1 * var(--space-2)) 0 var(--space-4); }
673
+ .more p { margin: 0 auto 0 0; }
674
+ .sort-note, .cut { margin: var(--space-2) 0; font-size: var(--text-sm); }
675
+ table.sessions tr.others td { color: var(--muted); font-style: italic; }
676
+ .toolbar .fields { display: contents; }
677
+ .toolbar .field.grow input { min-width: 14em; }
678
+ .toolbar select { min-width: 10em; max-width: 22em; }
679
+ .chart-card .card-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
680
+ .chart-card.note { display: block; }
681
+ .chart-card.note p.dim { margin: var(--space-4) 0; }
682
+ @media (max-width: 640px) {
683
+ /* an odd card last spans the row: no orphan beside an empty cell */
684
+ .cards > :last-child:nth-child(odd) { grid-column: 1 / -1; }
685
+ .toolbar .field, .toolbar .field.grow { flex: 1 1 100%; }
686
+ /* a field's control is its width, never its height (the phone's input { flex } is a row's, and a field is a column) */
687
+ .toolbar .field input, .toolbar .field select { flex: none; width: 100%; min-width: 0; max-width: none; }
688
+ .toolbar .actions { margin-left: 0; }
689
+ code.name { max-width: 22ch; }
690
+ }
691
+ form#request-filters input[type="checkbox"] { min-width: 0; }
692
+ #page-queries table.requests .clip { max-width: 9em; }
693
+ #page-queries table.requests td.stmt-cell { min-width: 12em; }
694
+ /* beside the full sidebar (1000–1100 px) or the rail, the log's and the running statements' names cut shorter: the table fits its card */
695
+ @media (max-width: 1100px) {
696
+ #page-queries table.requests .clip, #page-queries table.running .clip { max-width: 6em; }
697
+ #page-queries table.requests td.stmt-cell { min-width: 9em; }
698
+ }
699
+ #page-queries tr.detail pre { max-height: 320px; overflow: auto; }
700
+
701
+ /* ---------------------------------------------------------------- the unsupported SQL (Queries; a line on the Overview) */
702
+ ul.unsupported { list-style: none; margin: 0; padding: 0; }
703
+ ul.unsupported li { padding: var(--space-2) var(--space-3); border-top: 1px solid var(--line); }
704
+ ul.unsupported li:first-child { border-top: 0; }
705
+ ul.unsupported p { margin: 0; overflow-wrap: anywhere; }
706
+ ul.unsupported .headline { font-weight: 600; }
707
+ ul.unsupported .clip { max-width: min(40em, 100%); }
708
+ .unsupported-line { margin: var(--space-4) 0 0; overflow-wrap: anywhere; }
709
+ .unsupported-line a.see { margin-left: var(--space-2); font-size: var(--text-sm); }
710
+
711
+ /* a table's copy on the Tables page: the Overview's bar (copyBar) and the words mason wait prints, on its note line */
712
+ .copyline { display: inline-flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
713
+ .copyline .what-copy { color: var(--muted); }
714
+
715
+ /* a phone: the five ranges closer, so the control fits the page's width (it scrolls inside itself, never the page, if not) */
716
+ @media (max-width: 640px) { .range a, .range .off { padding: 0 var(--space-2); } }