devicectl-core 0.1.0__py3-none-any.whl
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.
- devicectl/__init__.py +18 -0
- devicectl/cli/__init__.py +1 -0
- devicectl/cli/command.py +95 -0
- devicectl/cli/exits.py +32 -0
- devicectl/cli/fanout.py +142 -0
- devicectl/cli/main.py +69 -0
- devicectl/cli/output.py +299 -0
- devicectl/cli/parser.py +80 -0
- devicectl/cli/report.py +86 -0
- devicectl/cli/target.py +26 -0
- devicectl/clock.py +57 -0
- devicectl/devtools/__init__.py +6 -0
- devicectl/devtools/frontlint.py +935 -0
- devicectl/devtools/htmcheck.py +396 -0
- devicectl/devtools/rendercheck.py +384 -0
- devicectl/doctor.py +112 -0
- devicectl/errors.py +68 -0
- devicectl/fields.py +564 -0
- devicectl/meta.py +64 -0
- devicectl/paths.py +40 -0
- devicectl/progress.py +77 -0
- devicectl/report.py +67 -0
- devicectl/testing.py +199 -0
- devicectl/trace.py +333 -0
- devicectl/web/__init__.py +1 -0
- devicectl/web/agents.py +94 -0
- devicectl/web/events.py +171 -0
- devicectl/web/http.py +243 -0
- devicectl/web/progress.py +101 -0
- devicectl/web/server.py +1013 -0
- devicectl/web/static/core.css +3034 -0
- devicectl/web/static/js/api.js +198 -0
- devicectl/web/static/js/band.js +640 -0
- devicectl/web/static/js/chart.js +400 -0
- devicectl/web/static/js/drafts.js +312 -0
- devicectl/web/static/js/notify.js +272 -0
- devicectl/web/static/js/panels.js +432 -0
- devicectl/web/static/js/shell.js +672 -0
- devicectl/web/static/js/trace.js +133 -0
- devicectl/web/static/js/ui.js +1139 -0
- devicectl/web/static/vendor/preact-htm.module.js +27 -0
- devicectl/web/worker.py +697 -0
- devicectl_core-0.1.0.dist-info/METADATA +131 -0
- devicectl_core-0.1.0.dist-info/RECORD +47 -0
- devicectl_core-0.1.0.dist-info/WHEEL +4 -0
- devicectl_core-0.1.0.dist-info/licenses/LICENSE +287 -0
- devicectl_core-0.1.0.dist-info/licenses/NOTICE +13 -0
|
@@ -0,0 +1,3034 @@
|
|
|
1
|
+
/* The design system: what a card, a row, a button and a dialog are made of.
|
|
2
|
+
*
|
|
3
|
+
* Every rule here is written against tokens and names no colour, so the
|
|
4
|
+
* same stylesheet dresses two programs that are meant to be told apart by
|
|
5
|
+
* their palette and by nothing else. A program supplies the colours in its
|
|
6
|
+
* own `app.css`, along with the panels that are genuinely its own.
|
|
7
|
+
*
|
|
8
|
+
* The tokens a program must define, in `:root` and again for the light
|
|
9
|
+
* theme -- all of them colours, and that is the point:
|
|
10
|
+
*
|
|
11
|
+
* surfaces --bg --surface --surface-2 --surface-3 --line --line-strong
|
|
12
|
+
* type --text --text-dim --text-faint
|
|
13
|
+
* colour --primary --primary-bright --primary-deep
|
|
14
|
+
* --live --live-deep something happening now
|
|
15
|
+
* --warn --warn-deep worth a look
|
|
16
|
+
* --ok --ok-bright --ok-deep settled, within limits
|
|
17
|
+
* --danger --danger-deep destructive, or a fault
|
|
18
|
+
*
|
|
19
|
+
* Two optional tokens. `--chart-alt` is the second line on a chart that
|
|
20
|
+
* carries two readings -- it defaults to `--primary-bright`, and exists so
|
|
21
|
+
* a program whose `--live` is already close to that can move it rather than
|
|
22
|
+
* draw two lines nobody can tell apart.
|
|
23
|
+
*
|
|
24
|
+
* And `--switch-on`: what a switch is filled with when it is on. It
|
|
25
|
+
* defaults to `--primary` and is a token because the two programs do not
|
|
26
|
+
* mean the same thing by a switch. In one they are settings -- a scan
|
|
27
|
+
* with the radio on, a backup that includes the certificates -- and a
|
|
28
|
+
* setting that is enabled is the page's own colour.
|
|
29
|
+
* In the other they are the pack's three main switches: charging on,
|
|
30
|
+
* discharging on, balancing on, which is the page's word for *within
|
|
31
|
+
* limits and working*, and that page paints that green wherever it says
|
|
32
|
+
* it. A blue switch there would be the one place on the page where "on"
|
|
33
|
+
* was not the colour everything else says "on" in.
|
|
34
|
+
*
|
|
35
|
+
* Every *measurement* is defined below instead -- the type scale, the
|
|
36
|
+
* radii, the gaps, how tight a card is, how wide a label column is, where
|
|
37
|
+
* the page's edges are. They were each program's own, and the result was
|
|
38
|
+
* two pages that shared a stylesheet and still did not line up: one had a
|
|
39
|
+
* 14px card padding and a 10.5rem label column, the other 18px and 9.5rem,
|
|
40
|
+
* so the same card was a different shape depending on which program drew
|
|
41
|
+
* it. A program that overrides one of these numbers now is saying
|
|
42
|
+
* something deliberate rather than repeating a default.
|
|
43
|
+
*
|
|
44
|
+
* The four hues used to be named for what they are -- blue, amber, green,
|
|
45
|
+
* rose -- which worked for one program and stopped working for two: both
|
|
46
|
+
* had all four and gave them different jobs, so a shared rule reading
|
|
47
|
+
* `var(--amber)` meant "live" in one page and "worth a look" in the other.
|
|
48
|
+
* Names are jobs here. `--warn` and `--live` are the same amber in both
|
|
49
|
+
* today, and are two tokens so that either program can part them later
|
|
50
|
+
* without touching this file.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/* --- the type scale ------------------------------------------------------- *
|
|
54
|
+
*
|
|
55
|
+
* The one thing two programs sharing a design system must not each decide
|
|
56
|
+
* for themselves. Both had written this block out with the same four
|
|
57
|
+
* numbers in it -- and then each had gone on to write a fifth somewhere
|
|
58
|
+
* else, a 14px table in one and a 13.5px tab strip in the other, until the
|
|
59
|
+
* same widget was a different size on the two pages.
|
|
60
|
+
*
|
|
61
|
+
* `--fs-base` is the body; the scale is two steps below it and one above.
|
|
62
|
+
* Nothing on either page is smaller than `--fs-xs`, because a device is
|
|
63
|
+
* read in a garage at night as often as at a desk.
|
|
64
|
+
*/
|
|
65
|
+
:root {
|
|
66
|
+
--fs-base: 15px;
|
|
67
|
+
--fs-xs: 12px;
|
|
68
|
+
--fs-sm: 13.5px;
|
|
69
|
+
--fs-lg: 17px;
|
|
70
|
+
--body-leading: 1.55;
|
|
71
|
+
|
|
72
|
+
/* A card's heading is an eyebrow rather than a title: small, letterspaced
|
|
73
|
+
* and quiet -- and it is the same eyebrow on both pages. One of them set
|
|
74
|
+
* it at 650 and 0.07em in the faint grey and the other at 600 and 0.09em
|
|
75
|
+
* in the dim one, which is two different headings on two pages that are
|
|
76
|
+
* supposed to be the same program wearing two colours. */
|
|
77
|
+
--card-title-size: var(--fs-xs);
|
|
78
|
+
--card-title-weight: 620;
|
|
79
|
+
--card-title-track: 0.08em;
|
|
80
|
+
--card-title-color: var(--text-dim);
|
|
81
|
+
--card-title-gap: 12px;
|
|
82
|
+
|
|
83
|
+
/* A toast is chrome and is set at the size the rest of the chrome is. The
|
|
84
|
+
* fallback here used to be an ad-hoc 14px that only one of the two pages
|
|
85
|
+
* ever overrode, which made a toast a different size in each. */
|
|
86
|
+
--toast-size: var(--fs-sm);
|
|
87
|
+
|
|
88
|
+
/* The two families. Both programs had named the same two stacks; one
|
|
89
|
+
* with a face of its own overrides them. */
|
|
90
|
+
--sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
|
91
|
+
--mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
|
92
|
+
|
|
93
|
+
/* --- the measurements ---------------------------------------------------
|
|
94
|
+
*
|
|
95
|
+
* Corners, gaps, and how much air a card, a row and the page's edges are
|
|
96
|
+
* given. Every one of these was written twice, with two different
|
|
97
|
+
* numbers, so a card in one program was 14px tight and in the other 18px
|
|
98
|
+
* loose, a label column was 10.5rem here and 9.5rem there, and the two
|
|
99
|
+
* pages did not line up when you put them side by side.
|
|
100
|
+
*
|
|
101
|
+
* The numbers below are one set, halfway between the two: dense enough
|
|
102
|
+
* for a page of forty readings and loose enough to read at arm's length
|
|
103
|
+
* in a garage.
|
|
104
|
+
*/
|
|
105
|
+
--radius: 10px;
|
|
106
|
+
--radius-sm: 6px;
|
|
107
|
+
--gap: 16px;
|
|
108
|
+
|
|
109
|
+
/* How wide the content is, and how far it stays off the window's edge.
|
|
110
|
+
* The header, the tab strip and the page all use these, so the wordmark,
|
|
111
|
+
* the first tab and the first card share one left edge. */
|
|
112
|
+
--page: 1600px;
|
|
113
|
+
--gutter: clamp(14px, 2.4vw, 40px);
|
|
114
|
+
|
|
115
|
+
/* A card: its padding, the gap between two rows in it, and how wide a
|
|
116
|
+
* column of the grid wants to be before another column is added. */
|
|
117
|
+
--card-pad: 16px 18px;
|
|
118
|
+
--row-gap: 8px;
|
|
119
|
+
--grid-track: 340px;
|
|
120
|
+
|
|
121
|
+
/* The width of a field's name, everywhere on both pages. One number is
|
|
122
|
+
* what makes values line up down a page instead of stepping in and out
|
|
123
|
+
* with whatever the longest label in each card happened to be -- and one
|
|
124
|
+
* number *across* the two pages is what makes them the same program. */
|
|
125
|
+
--label-col: 10rem;
|
|
126
|
+
|
|
127
|
+
--modal-width: 640px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* What the browser draws its own widgets in -- a number input's spinners, a
|
|
131
|
+
* select's arrow and popup, a scrollbar, a range slider's track. `useTheme`
|
|
132
|
+
* in js/shell.js always stamps `data-theme`, from the system's own
|
|
133
|
+
* preference unless this browser has been told otherwise, so this follows
|
|
134
|
+
* the page rather than the machine: a laptop set to dark was putting black
|
|
135
|
+
* widgets on a light page in the one program that had forgotten to say
|
|
136
|
+
* this. Checkboxes are no longer among them -- see `label.toggle`, which
|
|
137
|
+
* draws its own. */
|
|
138
|
+
:root {
|
|
139
|
+
color-scheme: dark;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
:root[data-theme="light"] {
|
|
143
|
+
color-scheme: light;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* --- base ---------------------------------------------------------------- */
|
|
147
|
+
|
|
148
|
+
* {
|
|
149
|
+
box-sizing: border-box;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
html,
|
|
153
|
+
body {
|
|
154
|
+
margin: 0;
|
|
155
|
+
height: 100%;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
body {
|
|
159
|
+
background: var(--bg);
|
|
160
|
+
color: var(--text);
|
|
161
|
+
font: var(--fs-base)/var(--body-leading) var(--sans);
|
|
162
|
+
-webkit-font-smoothing: antialiased;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
button,
|
|
166
|
+
input,
|
|
167
|
+
select,
|
|
168
|
+
textarea {
|
|
169
|
+
font: inherit;
|
|
170
|
+
color: inherit;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
a {
|
|
174
|
+
color: var(--primary);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
:focus-visible {
|
|
178
|
+
outline: 2px solid var(--primary);
|
|
179
|
+
outline-offset: 2px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Every number that changes under you keeps its columns still. */
|
|
183
|
+
.value,
|
|
184
|
+
table td,
|
|
185
|
+
.stat .v,
|
|
186
|
+
.pill-op {
|
|
187
|
+
font-variant-numeric: tabular-nums;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.muted {
|
|
191
|
+
color: var(--text-faint);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* Something read character by character -- a device path, a register name,
|
|
195
|
+
* a command you are meant to type. Both pages had this helper, at the same
|
|
196
|
+
* two declarations, under the same name. */
|
|
197
|
+
.mono {
|
|
198
|
+
font-family: var(--mono);
|
|
199
|
+
font-size: var(--fs-sm);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.spacer {
|
|
203
|
+
flex: 1 1 auto;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.grow {
|
|
207
|
+
flex: 1 1 auto;
|
|
208
|
+
min-width: 180px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.grow.narrow {
|
|
212
|
+
flex: 1 1 90px;
|
|
213
|
+
min-width: 90px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.small {
|
|
217
|
+
font-size: var(--fs-sm);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.spaced {
|
|
221
|
+
margin-top: 12px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Said to a screen reader and to nothing else. Not `display: none`, which
|
|
225
|
+
* is not read at all. */
|
|
226
|
+
.sr-only {
|
|
227
|
+
position: absolute;
|
|
228
|
+
width: 1px;
|
|
229
|
+
height: 1px;
|
|
230
|
+
margin: -1px;
|
|
231
|
+
padding: 0;
|
|
232
|
+
overflow: hidden;
|
|
233
|
+
clip-path: inset(50%);
|
|
234
|
+
white-space: nowrap;
|
|
235
|
+
border: 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* --- the grid of cards --------------------------------------------------- */
|
|
239
|
+
|
|
240
|
+
/* One card is one column, always. auto-fill (not auto-fit) keeps the track
|
|
241
|
+
* width fixed by the window rather than by how many cards happen to exist,
|
|
242
|
+
* so every panel on the page is the same width and a page with five of them
|
|
243
|
+
* looks like a page with eight.
|
|
244
|
+
*
|
|
245
|
+
* The cards used to keep their own heights, which meant a row of them ended
|
|
246
|
+
* on three different lines and the page read as a pile rather than a grid.
|
|
247
|
+
* They stretch to their row now, and the card is a column so that what sits
|
|
248
|
+
* at the bottom of one -- the Apply row, the caveats -- sits at the bottom
|
|
249
|
+
* of all of them rather than wherever its own content happened to stop. */
|
|
250
|
+
.grid {
|
|
251
|
+
display: grid;
|
|
252
|
+
gap: var(--gap);
|
|
253
|
+
grid-template-columns: repeat(auto-fill, minmax(var(--grid-track, 360px), 1fr));
|
|
254
|
+
align-items: stretch;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.card {
|
|
258
|
+
display: flex;
|
|
259
|
+
flex-direction: column;
|
|
260
|
+
min-width: 0;
|
|
261
|
+
background: var(--surface);
|
|
262
|
+
border: 1px solid var(--line);
|
|
263
|
+
border-radius: var(--radius);
|
|
264
|
+
padding: var(--card-pad);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Every card renders this heading through the one shared `Card`, so its
|
|
268
|
+
* layout lives here rather than as an inline style the class checks cannot
|
|
269
|
+
* see: the title, then a spacer, then whatever the card hangs on the right
|
|
270
|
+
* of it -- a badge saying what it is, then the actions that work on it. */
|
|
271
|
+
.card > h2 {
|
|
272
|
+
display: flex;
|
|
273
|
+
align-items: center;
|
|
274
|
+
gap: 10px;
|
|
275
|
+
margin: 0 0 var(--card-title-gap, 14px);
|
|
276
|
+
font-size: var(--card-title-size);
|
|
277
|
+
font-weight: var(--card-title-weight, 600);
|
|
278
|
+
letter-spacing: var(--card-title-track);
|
|
279
|
+
text-transform: uppercase;
|
|
280
|
+
color: var(--card-title-color);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.card > h2 .grow {
|
|
284
|
+
flex: 1 1 auto;
|
|
285
|
+
min-width: 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* Nothing beside the title makes the title taller. A badge, a button, the
|
|
289
|
+
* unsent count and its Apply are all taller than a line of small capitals,
|
|
290
|
+
* and each one that came and went -- the count with the first edit, the
|
|
291
|
+
* Apply with it -- moved everything under the title by the difference. So
|
|
292
|
+
* they overhang the line instead, centred on it, into the card's padding
|
|
293
|
+
* above and the title's gap below; every title is the height of its text,
|
|
294
|
+
* whatever is hung on it. */
|
|
295
|
+
.card > h2 > :not(.grow) {
|
|
296
|
+
flex: none;
|
|
297
|
+
margin-block: -8px;
|
|
298
|
+
white-space: nowrap;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* And the title keeps to its one line while the count and its Apply are
|
|
302
|
+
* beside it: on a phone a long title would otherwise wrap under them, and
|
|
303
|
+
* the card would grow by a line with the first edit. It is cut short
|
|
304
|
+
* instead, for as long as the card holds edits -- the edge and the count
|
|
305
|
+
* say which card it is meanwhile. */
|
|
306
|
+
.card.pending > h2 .grow {
|
|
307
|
+
overflow: hidden;
|
|
308
|
+
white-space: nowrap;
|
|
309
|
+
text-overflow: ellipsis;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.card-body {
|
|
313
|
+
flex: 1 1 auto;
|
|
314
|
+
min-width: 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* A button that is a card's own child -- "Restart the device", "Sync the
|
|
318
|
+
* clock" -- is as wide as its label, not as wide as the card. It used to
|
|
319
|
+
* be that by being inline in a block; now that the card is a flex column
|
|
320
|
+
* it has to be told, or a stretched item is what it becomes. */
|
|
321
|
+
.card-body > button.btn {
|
|
322
|
+
align-self: flex-start;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/* Two exceptions to a card per column, and they are the only two. `wide`
|
|
326
|
+
* is content that wants more than one track but not the row -- a list of
|
|
327
|
+
* releases, a four-column table; `full` is content that is genuinely a row
|
|
328
|
+
* wide. Anything else stays one column, which is what keeps the page a
|
|
329
|
+
* grid.
|
|
330
|
+
*
|
|
331
|
+
* A full-width card *ends* its row, so every card after a short one
|
|
332
|
+
* spanning the page starts a new line -- which is what makes a tab read as
|
|
333
|
+
* a stack rather than a grid. `tools/frontlint.py` (C005) holds a card
|
|
334
|
+
* claiming the row to holding a table, a log or a plot.
|
|
335
|
+
*
|
|
336
|
+
* What is *not* here is `grid-auto-flow: dense`. It would let a later
|
|
337
|
+
* one-column card backfill the gap a wide one leaves at the end of a row,
|
|
338
|
+
* and it would do it by decoupling visual order from DOM order -- on a
|
|
339
|
+
* page whose tab strip is a real `role="tablist"` with a roving tabIndex.
|
|
340
|
+
* Keyboard order disagreeing with reading order is a worse fault than a
|
|
341
|
+
* gap. Cards that really are a row wide go last within their tab instead,
|
|
342
|
+
* so the row they end is the last one. */
|
|
343
|
+
.card.wide {
|
|
344
|
+
grid-column: span 2;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.card.full {
|
|
348
|
+
grid-column: 1 / -1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
@media (max-width: 900px) {
|
|
352
|
+
.card.wide {
|
|
353
|
+
grid-column: 1 / -1;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* --- which device this page could be about ---------------------------------
|
|
358
|
+
*
|
|
359
|
+
* A grid of tiles, one per device the server can see: the boards on a bus,
|
|
360
|
+
* the chargers on a network. Clicking one is how the page changes what it
|
|
361
|
+
* is about, and the same grid is behind the button in the header that opens
|
|
362
|
+
* the picker -- so the list you choose from is the list you were looking at.
|
|
363
|
+
*
|
|
364
|
+
* A tile is a button, is as wide as the grid gives it, and carries a
|
|
365
|
+
* coloured left edge that is the whole point of the view: four packs, four
|
|
366
|
+
* states of charge, and the one that is drifting visible without opening
|
|
367
|
+
* anything.
|
|
368
|
+
*/
|
|
369
|
+
.tiles {
|
|
370
|
+
display: grid;
|
|
371
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
372
|
+
gap: var(--gap);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.tile {
|
|
376
|
+
display: block;
|
|
377
|
+
width: 100%;
|
|
378
|
+
text-align: left;
|
|
379
|
+
color: inherit;
|
|
380
|
+
background: var(--surface);
|
|
381
|
+
border: 1px solid var(--line);
|
|
382
|
+
/* Neutral until the device has said something. A green edge on a tile
|
|
383
|
+
* for a charger nobody has connected to yet is the page claiming to know
|
|
384
|
+
* it is well. */
|
|
385
|
+
border-left: 3px solid var(--line-strong);
|
|
386
|
+
border-radius: var(--radius);
|
|
387
|
+
padding: 13px 15px;
|
|
388
|
+
cursor: pointer;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.tile:hover {
|
|
392
|
+
border-color: var(--primary);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.tile.good {
|
|
396
|
+
border-left-color: var(--ok);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.tile.warn {
|
|
400
|
+
border-left-color: var(--warn);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.tile.bad {
|
|
404
|
+
border-left-color: var(--danger);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.tile.gone {
|
|
408
|
+
border-left-color: var(--line-strong);
|
|
409
|
+
opacity: 0.75;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.tile.selected {
|
|
413
|
+
outline: 1px solid var(--primary);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.tile-head {
|
|
417
|
+
display: flex;
|
|
418
|
+
align-items: baseline;
|
|
419
|
+
gap: 8px;
|
|
420
|
+
margin-bottom: 8px;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.tile-head .id {
|
|
424
|
+
font-weight: 700;
|
|
425
|
+
font-size: var(--fs-lg);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* Nothing is written until this row says so: a card of settings is edited
|
|
429
|
+
* as a whole and applied once, so a stray keystroke cannot reach a device
|
|
430
|
+
* that is mid-charge. */
|
|
431
|
+
.card-foot {
|
|
432
|
+
display: flex;
|
|
433
|
+
align-items: center;
|
|
434
|
+
flex-wrap: wrap;
|
|
435
|
+
gap: 10px;
|
|
436
|
+
/* `auto` rather than a fixed margin: the cards in a row are one height,
|
|
437
|
+
* so the row that commits a card's edits belongs against its bottom
|
|
438
|
+
* edge, not floating wherever the fields above it happened to end. */
|
|
439
|
+
margin-top: auto;
|
|
440
|
+
padding-top: 13px;
|
|
441
|
+
border-top: 1px solid var(--line);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* The gap the footer's own margin used to hold open, moved to the element
|
|
445
|
+
* above it -- an `auto` margin spends itself on the free space and has
|
|
446
|
+
* nothing left over for the card that is already as tall as its row.
|
|
447
|
+
*
|
|
448
|
+
* `:not(:empty)` for the same reason the rule below folds an empty foot
|
|
449
|
+
* away: an Apply bar that appears only once something is edited leaves the
|
|
450
|
+
* element behind, and a gap held open for a rule that is not being drawn
|
|
451
|
+
* is 16px of nothing at the bottom of every card that can be edited. */
|
|
452
|
+
.card-body:has(+ .card-foot:not(:empty)) {
|
|
453
|
+
margin-bottom: 16px;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/* A foot whose contents are conditional -- an Apply bar that appears only
|
|
457
|
+
* once something is edited -- still leaves the element behind. A rule
|
|
458
|
+
* ruling off nothing reads as a card that failed to finish drawing. */
|
|
459
|
+
.card-foot:empty {
|
|
460
|
+
display: none;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.card-foot .state {
|
|
464
|
+
color: var(--text-faint);
|
|
465
|
+
font-size: var(--fs-sm);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* --- rows ---------------------------------------------------------------- */
|
|
469
|
+
|
|
470
|
+
/* Every row in a card shares one two-column grid, so all the values in a
|
|
471
|
+
* card start at the same place. The width of that column is the same
|
|
472
|
+
* number in every card: derived from the content it made "Mode" and "Clock
|
|
473
|
+
* difference" put their values in different places, and a page of cards had
|
|
474
|
+
* as many value columns as it had cards. A fixed column is one column,
|
|
475
|
+
* down the whole page.
|
|
476
|
+
*
|
|
477
|
+
* The label sits against the middle of its value, not against its first
|
|
478
|
+
* baseline. Baseline alignment is right for two pieces of text and wrong
|
|
479
|
+
* for everything else a value can be: a `<select>`, a range slider and a
|
|
480
|
+
* checkbox each put their baseline somewhere different -- the bottom of
|
|
481
|
+
* the box, the middle of the thumb -- so a card of mixed rows had the
|
|
482
|
+
* labels at four heights and none of them level with the control they
|
|
483
|
+
* name. Centred, every label is level with its value whatever the value
|
|
484
|
+
* is made of. */
|
|
485
|
+
.rows {
|
|
486
|
+
display: grid;
|
|
487
|
+
grid-template-columns: minmax(0, var(--label-col)) minmax(0, 1fr);
|
|
488
|
+
gap: var(--row-gap, 9px) 16px;
|
|
489
|
+
align-items: center;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/* A row that is not in a row list is a two-column grid of its own.
|
|
493
|
+
*
|
|
494
|
+
* It was only ever `display: contents`, which is how a pair dissolves into
|
|
495
|
+
* the list's shared grid -- and does nothing whatever when there is no list
|
|
496
|
+
* around it. Put straight into a card, which is a flex *column*, the label
|
|
497
|
+
* and the value became two stacked items: the name on one line and the
|
|
498
|
+
* value right-aligned on the next. Whole tabs were built that way while
|
|
499
|
+
* others wrapped their rows and got two columns, which is why the same kind
|
|
500
|
+
* of field looked like two different things depending on which tab you were
|
|
501
|
+
* on.
|
|
502
|
+
*
|
|
503
|
+
* The columns are identical either way, because the label column is a fixed
|
|
504
|
+
* width rather than one derived from the content -- so a row aligns with
|
|
505
|
+
* the rows above it whether they share one grid or have one each, and a
|
|
506
|
+
* card can no longer be laid out wrongly by forgetting a wrapper. */
|
|
507
|
+
.row {
|
|
508
|
+
display: grid;
|
|
509
|
+
grid-template-columns: minmax(0, var(--label-col)) minmax(0, 1fr);
|
|
510
|
+
gap: var(--row-gap, 9px) 16px;
|
|
511
|
+
align-items: center;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.row + .row {
|
|
515
|
+
margin-top: var(--row-gap, 9px);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.rows .row {
|
|
519
|
+
display: contents;
|
|
520
|
+
margin-top: 0;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/* On a phone, the label above its value: a key, a URL or a timestamp gets
|
|
524
|
+
* the whole width of the card rather than a third of it.
|
|
525
|
+
*
|
|
526
|
+
* Stacked there is no column of values for one to line up with, so the
|
|
527
|
+
* right alignment that makes two columns tidy is dropped along with the
|
|
528
|
+
* second column -- a value pushed to the far edge from the label above it
|
|
529
|
+
* only looks lost. Without this the second column keeps a minimum of its
|
|
530
|
+
* own and the inputs in it run off the side of the page. */
|
|
531
|
+
@media (max-width: 560px) {
|
|
532
|
+
.rows,
|
|
533
|
+
.row {
|
|
534
|
+
grid-template-columns: minmax(0, 1fr);
|
|
535
|
+
gap: 2px;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.rows .row + .row .k,
|
|
539
|
+
.row + .row {
|
|
540
|
+
margin-top: 9px;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.v {
|
|
544
|
+
text-align: left;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.v > .set,
|
|
548
|
+
.v > .toggle,
|
|
549
|
+
.v > .chips,
|
|
550
|
+
.v > .actions {
|
|
551
|
+
justify-content: flex-start;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/* A row list is a two-column grid whose pairs dissolve into it, so anything
|
|
556
|
+
* else put directly in one is a grid item and auto-placement drops it in
|
|
557
|
+
* whichever single column happened to be free -- usually the label column,
|
|
558
|
+
* which wraps a price list to death. Give such a block the whole width; a
|
|
559
|
+
* group of rows belongs flat among the rows anyway. */
|
|
560
|
+
.rows > :not(.row) {
|
|
561
|
+
grid-column: 1 / -1;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.row .k {
|
|
565
|
+
color: var(--text-dim);
|
|
566
|
+
font-size: var(--fs-sm);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/* A name with something to say about itself on hover -- an acronym spelled
|
|
570
|
+
* out, the register behind it. Dotted, the way an abbreviation is marked,
|
|
571
|
+
* so the pointer is told there is something there without the name being
|
|
572
|
+
* made to look like a link. */
|
|
573
|
+
.row .k.hinted {
|
|
574
|
+
cursor: help;
|
|
575
|
+
text-decoration: underline dotted color-mix(in srgb, var(--text-dim) 55%, transparent);
|
|
576
|
+
text-underline-offset: 3px;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/* Values end at the card's right edge rather than starting at the label's.
|
|
580
|
+
* Left-aligned they left a different width of nothing after every one, so a
|
|
581
|
+
* card of six readings had six ragged ends; against the edge they line up
|
|
582
|
+
* with each other, with the values in the card below, and -- the cards
|
|
583
|
+
* being one width -- across the page. */
|
|
584
|
+
.row .v {
|
|
585
|
+
color: var(--text);
|
|
586
|
+
min-width: 0;
|
|
587
|
+
overflow-wrap: anywhere;
|
|
588
|
+
text-align: right;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/* A value that is itself a row of things -- a slider and its reading, a
|
|
592
|
+
* checkbox and its label -- has to be pushed rather than aligned. */
|
|
593
|
+
.row .v > .set,
|
|
594
|
+
.row .v > .toggle,
|
|
595
|
+
.row .v > .chips,
|
|
596
|
+
.row .v > .wrap {
|
|
597
|
+
justify-content: flex-end;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/* Identifiers, keys and timestamps read character by character, so they get
|
|
601
|
+
* fixed-width digits. They wrap like anything else: a value that runs off
|
|
602
|
+
* its own scrollbar is worse than one that takes two lines. */
|
|
603
|
+
.row .v.data,
|
|
604
|
+
.row .v.token {
|
|
605
|
+
font-family: var(--mono);
|
|
606
|
+
font-size: var(--fs-sm);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.row .v.good {
|
|
610
|
+
color: var(--ok);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.row .v.warn {
|
|
614
|
+
color: var(--warn);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.row .v.bad {
|
|
618
|
+
color: var(--danger);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* --- badges, stats and empties -------------------------------------------- */
|
|
622
|
+
|
|
623
|
+
.badge {
|
|
624
|
+
font-size: var(--fs-xs);
|
|
625
|
+
letter-spacing: 0.05em;
|
|
626
|
+
text-transform: uppercase;
|
|
627
|
+
padding: 2px 7px;
|
|
628
|
+
border-radius: 4px;
|
|
629
|
+
border: 1px solid var(--line-strong);
|
|
630
|
+
color: var(--text-dim);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.badge.warn {
|
|
634
|
+
color: var(--warn);
|
|
635
|
+
border-color: var(--warn-deep);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.badge.good {
|
|
639
|
+
color: var(--ok);
|
|
640
|
+
border-color: var(--ok-deep);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.badge.bad {
|
|
644
|
+
color: var(--danger);
|
|
645
|
+
border-color: var(--danger-deep);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.badge.info {
|
|
649
|
+
color: var(--primary);
|
|
650
|
+
border-color: var(--primary-deep);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/* How many edits a card is holding that have not been written. In the
|
|
654
|
+
* colour every unsent edit on the page wears -- the field's edge, the dot
|
|
655
|
+
* on a band -- so the card, the row and the control all say the same thing
|
|
656
|
+
* in the same colour. See `Card` in js/ui.js. */
|
|
657
|
+
.badge.unsent {
|
|
658
|
+
color: var(--live);
|
|
659
|
+
border-color: var(--live);
|
|
660
|
+
white-space: nowrap;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/* A card's own Apply and Discard, in its title (see `CardApply`): two
|
|
664
|
+
* squares the height of the badge beside them. Like everything else hung
|
|
665
|
+
* on a title they take none of its height (`.card > h2 > :not(.grow)`). */
|
|
666
|
+
.card-apply {
|
|
667
|
+
display: inline-flex;
|
|
668
|
+
gap: 4px;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
button.btn.tick {
|
|
672
|
+
display: inline-flex;
|
|
673
|
+
align-items: center;
|
|
674
|
+
justify-content: center;
|
|
675
|
+
width: 26px;
|
|
676
|
+
height: 24px;
|
|
677
|
+
padding: 0;
|
|
678
|
+
line-height: 1;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/* A card holding unsent edits, from across the page: its edge changes
|
|
682
|
+
* colour. A colour on an edge the card already has, so nothing in it or
|
|
683
|
+
* around it moves when the first edit is made or the last one sent. */
|
|
684
|
+
.card.pending {
|
|
685
|
+
border-color: color-mix(in srgb, var(--live) 70%, var(--line));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/* A labelled reading, in a row of them: what it is in small caps, the
|
|
689
|
+
* number under it. Pack voltage, current, power; average cell, spread,
|
|
690
|
+
* balance current. Three or six of these under a card's title is how both
|
|
691
|
+
* pages say "here are the figures" before anything has to be read.
|
|
692
|
+
*
|
|
693
|
+
* It was a bordered pill here and a plain stacked pair in the other
|
|
694
|
+
* program, under the same class name -- so a stylesheet rule written
|
|
695
|
+
* against `.stat` meant two different widgets depending on which page it
|
|
696
|
+
* landed on. This is the stacked one: a pill of two words is a badge, and
|
|
697
|
+
* `.badge` is already that.
|
|
698
|
+
*
|
|
699
|
+
* `.k` and `.v` rather than `.label` and a bare div, because that is what a
|
|
700
|
+
* row's two halves are called everywhere else on both pages.
|
|
701
|
+
*/
|
|
702
|
+
.stats {
|
|
703
|
+
display: flex;
|
|
704
|
+
flex-wrap: wrap;
|
|
705
|
+
gap: 6px 18px;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/* As wide as what it says, and no wider. A floor under this -- these were
|
|
709
|
+
* 90px each -- is what made a row of three readings wrap to two and one
|
|
710
|
+
* inside a tile, which reads as a layout that gave up rather than as three
|
|
711
|
+
* figures. */
|
|
712
|
+
.stat {
|
|
713
|
+
min-width: 0;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.stat .k {
|
|
717
|
+
display: block;
|
|
718
|
+
font-size: var(--fs-xs);
|
|
719
|
+
color: var(--text-faint);
|
|
720
|
+
text-transform: uppercase;
|
|
721
|
+
letter-spacing: 0.05em;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.stat .v {
|
|
725
|
+
color: var(--text);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.stat.warn .v {
|
|
729
|
+
color: var(--warn);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.stat.good .v {
|
|
733
|
+
color: var(--ok);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.stat.bad .v {
|
|
737
|
+
color: var(--danger);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/* Nothing here, said in the space one sentence needs. This was 30px of
|
|
741
|
+
* padding above and below, which on a tab whose empty state is the whole
|
|
742
|
+
* page until something is read is a card mostly made of nothing, above the
|
|
743
|
+
* one control that would fill it. */
|
|
744
|
+
.empty {
|
|
745
|
+
color: var(--text-dim);
|
|
746
|
+
padding: 16px 4px;
|
|
747
|
+
text-align: center;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/* Explanatory prose under a card's title -- read as text, not a caption. */
|
|
751
|
+
.note {
|
|
752
|
+
color: var(--text-dim);
|
|
753
|
+
font-size: var(--fs-sm);
|
|
754
|
+
margin: 0 0 14px;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* Prose that carries its own spacing already -- inside a dialog, or beside
|
|
758
|
+
* a control in a toolbar. */
|
|
759
|
+
.note.flush {
|
|
760
|
+
margin: 0;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/* --- tables ---------------------------------------------------------------
|
|
764
|
+
*
|
|
765
|
+
* A catalogue of four hundred properties, a bank's register tables, a
|
|
766
|
+
* year's sessions: both pages are half made of tables, and both had drawn
|
|
767
|
+
* their own. One set its head in the lifted grey with 10px of padding and
|
|
768
|
+
* the other in the panel fill with 4px, one ruled every row in the line
|
|
769
|
+
* colour and the other at 55% of it -- so the same table was two tables.
|
|
770
|
+
*
|
|
771
|
+
* `.table-wrap` is the scroller a wide table goes in; a table that is
|
|
772
|
+
* taller than it is wide goes in a `.tall` instead, whose head stays put.
|
|
773
|
+
*/
|
|
774
|
+
.table-wrap {
|
|
775
|
+
overflow-x: auto;
|
|
776
|
+
border: 1px solid var(--line);
|
|
777
|
+
border-radius: var(--radius);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.tall {
|
|
781
|
+
max-height: 62vh;
|
|
782
|
+
overflow: auto;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
table {
|
|
786
|
+
width: 100%;
|
|
787
|
+
border-collapse: collapse;
|
|
788
|
+
font-size: var(--fs-sm);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
thead th {
|
|
792
|
+
position: sticky;
|
|
793
|
+
top: 0;
|
|
794
|
+
background: var(--surface-2);
|
|
795
|
+
text-align: left;
|
|
796
|
+
font-weight: 560;
|
|
797
|
+
font-size: var(--fs-xs);
|
|
798
|
+
letter-spacing: 0.06em;
|
|
799
|
+
text-transform: uppercase;
|
|
800
|
+
color: var(--text-dim);
|
|
801
|
+
padding: 9px 10px;
|
|
802
|
+
border-bottom: 1px solid var(--line);
|
|
803
|
+
white-space: nowrap;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
tbody td {
|
|
807
|
+
padding: 7px 10px;
|
|
808
|
+
border-bottom: 1px solid var(--line);
|
|
809
|
+
vertical-align: top;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
tbody tr:last-child td {
|
|
813
|
+
border-bottom: 0;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
tbody tr:hover {
|
|
817
|
+
background: var(--surface);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/* A column of figures: fixed-width digits, one line, and capped rather than
|
|
821
|
+
* allowed to set the table's width. One long value -- thirty-two cell
|
|
822
|
+
* voltages on one line -- would otherwise take a fifth of the page and
|
|
823
|
+
* squeeze the column that says what the row is down to one word per line.
|
|
824
|
+
* The cap is wide enough for this column's own editor, an input and its
|
|
825
|
+
* unit, and no wider. */
|
|
826
|
+
th.num,
|
|
827
|
+
td.num,
|
|
828
|
+
td.val.num {
|
|
829
|
+
text-align: right;
|
|
830
|
+
font-variant-numeric: tabular-nums;
|
|
831
|
+
white-space: nowrap;
|
|
832
|
+
max-width: 14rem;
|
|
833
|
+
overflow: hidden;
|
|
834
|
+
text-overflow: ellipsis;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/* The action column's buttons end where the table does. */
|
|
838
|
+
td.right,
|
|
839
|
+
th.right {
|
|
840
|
+
text-align: right;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/* Two buttons in one action cell -- "show" then "apply" -- are two
|
|
844
|
+
* controls, not one word. */
|
|
845
|
+
td.right .btn + .btn {
|
|
846
|
+
margin-left: 7px;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/* The columns that hold four characters and a button: `width: 1%` with
|
|
850
|
+
* nothing to wrap is the shrink-to-fit idiom, so the thing the table is
|
|
851
|
+
* about gets the width instead. */
|
|
852
|
+
td.type,
|
|
853
|
+
th.type,
|
|
854
|
+
td.right,
|
|
855
|
+
th.right {
|
|
856
|
+
width: 1%;
|
|
857
|
+
white-space: nowrap;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/* --- what folds ----------------------------------------------------------- */
|
|
861
|
+
|
|
862
|
+
/* A card's one line, with the paragraph behind it: what is worth reading
|
|
863
|
+
* every time stays out, what is worth reading once folds away. */
|
|
864
|
+
.help {
|
|
865
|
+
margin: 0 0 14px;
|
|
866
|
+
font-size: var(--fs-sm);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.help > summary {
|
|
870
|
+
display: flex;
|
|
871
|
+
align-items: baseline;
|
|
872
|
+
gap: 8px;
|
|
873
|
+
cursor: pointer;
|
|
874
|
+
color: var(--text-dim);
|
|
875
|
+
list-style: none;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.help > summary::-webkit-details-marker {
|
|
879
|
+
display: none;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.help > summary:hover {
|
|
883
|
+
color: var(--text);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
.help .short {
|
|
887
|
+
flex: 1 1 auto;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
.help > p {
|
|
891
|
+
margin: 7px 0 0;
|
|
892
|
+
color: var(--text-faint);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/* Something legal that will not do what it looks like. The card has one
|
|
896
|
+
* line for it; the whole explanation is the hover title and opens in place
|
|
897
|
+
* on a click, for touch and for keyboards. */
|
|
898
|
+
/* One line of warning at the foot of a card: a mark and a sentence. */
|
|
899
|
+
.caveat {
|
|
900
|
+
font-size: var(--fs-sm);
|
|
901
|
+
color: var(--warn);
|
|
902
|
+
margin-top: 8px;
|
|
903
|
+
display: flex;
|
|
904
|
+
gap: 7px;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/* A list of short lines -- findings, a plan's changes -- ruled between. */
|
|
908
|
+
.list {
|
|
909
|
+
margin: 0;
|
|
910
|
+
padding: 0;
|
|
911
|
+
list-style: none;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.list > li {
|
|
915
|
+
padding: 5px 0;
|
|
916
|
+
border-bottom: 1px solid color-mix(in srgb, var(--line) 55%, transparent);
|
|
917
|
+
display: flex;
|
|
918
|
+
flex-wrap: wrap;
|
|
919
|
+
gap: 8px;
|
|
920
|
+
align-items: baseline;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.list > li:last-child {
|
|
924
|
+
border-bottom: 0;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/* A plan covering several cards names each before its lines. */
|
|
928
|
+
.plan-head {
|
|
929
|
+
margin: 12px 0 2px;
|
|
930
|
+
font-size: var(--fs-xs);
|
|
931
|
+
font-weight: 600;
|
|
932
|
+
text-transform: uppercase;
|
|
933
|
+
letter-spacing: var(--card-title-track);
|
|
934
|
+
color: var(--text-dim);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.caveats {
|
|
938
|
+
display: grid;
|
|
939
|
+
gap: 6px;
|
|
940
|
+
margin-top: 12px;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.why {
|
|
944
|
+
font-size: var(--fs-sm);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.why > summary {
|
|
948
|
+
display: flex;
|
|
949
|
+
align-items: baseline;
|
|
950
|
+
gap: 7px;
|
|
951
|
+
cursor: help;
|
|
952
|
+
color: var(--warn);
|
|
953
|
+
list-style: none;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
.why > summary::-webkit-details-marker {
|
|
957
|
+
display: none;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
.why .mark {
|
|
961
|
+
flex: none;
|
|
962
|
+
width: 15px;
|
|
963
|
+
height: 15px;
|
|
964
|
+
border: 1px solid var(--warn);
|
|
965
|
+
border-radius: 50%;
|
|
966
|
+
font-size: 10px;
|
|
967
|
+
line-height: 13px;
|
|
968
|
+
text-align: center;
|
|
969
|
+
align-self: center;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
.why .short {
|
|
973
|
+
flex: 1 1 auto;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.why > p {
|
|
977
|
+
margin: 6px 0 0 22px;
|
|
978
|
+
color: var(--text-dim);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/* What says a line opens, wherever one does.
|
|
982
|
+
*
|
|
983
|
+
* This was the word "more" on a card's summary and the word "why?" on a
|
|
984
|
+
* caveat, set small and faint at the right-hand end -- two different labels
|
|
985
|
+
* for one gesture, neither of which looks like anything until it is read,
|
|
986
|
+
* and both of which vanished once the line was open so that the way to
|
|
987
|
+
* close it again was to guess. A chevron is the disclosure control every
|
|
988
|
+
* reader already knows, and it turns rather than disappearing -- so the
|
|
989
|
+
* shape says which way the line is now. */
|
|
990
|
+
.chev {
|
|
991
|
+
flex: none;
|
|
992
|
+
/* A rotation needs a box to turn, and the summaries these sit in align
|
|
993
|
+
* on the baseline -- which is not where the middle of a chevron is. */
|
|
994
|
+
display: inline-block;
|
|
995
|
+
align-self: center;
|
|
996
|
+
/* At the size of the letters next to it. It was `--fs-xs` on a Unicode
|
|
997
|
+
* "small triangle", which is a small shape inside a small em: about
|
|
998
|
+
* four pixels of mark, which is a dot. A drawn chevron holds its shape
|
|
999
|
+
* at any size, so it is set in the surrounding text's own size and the
|
|
1000
|
+
* stroke keeps it one weight lighter than the words. */
|
|
1001
|
+
width: 1em;
|
|
1002
|
+
height: 1em;
|
|
1003
|
+
/* Where one of these sits in running text rather than in a flex row --
|
|
1004
|
+
* in front of a key in a table of rows that open -- an inline-block sits
|
|
1005
|
+
* on the baseline, which puts a symmetrical mark well above the middle
|
|
1006
|
+
* of the letters it is in front of. */
|
|
1007
|
+
vertical-align: -0.15em;
|
|
1008
|
+
color: var(--text-faint);
|
|
1009
|
+
transition: transform 0.15s ease;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/* Turned a quarter turn, for a control that opens downwards. */
|
|
1013
|
+
.chev.down {
|
|
1014
|
+
transform: rotate(90deg);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
summary:hover > .chev {
|
|
1018
|
+
color: var(--text-dim);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
details[open] > summary > .chev {
|
|
1022
|
+
transform: rotate(90deg);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1026
|
+
.chev {
|
|
1027
|
+
transition: none;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/* --- controls -------------------------------------------------------------- */
|
|
1032
|
+
|
|
1033
|
+
/* Buttons carry the primary colour on their edge and a little of it in
|
|
1034
|
+
* their fill. Set in the same face and weight as a field value -- which is
|
|
1035
|
+
* what they sit next to -- they read as another line of data until you try
|
|
1036
|
+
* to press one. */
|
|
1037
|
+
button.btn {
|
|
1038
|
+
background: color-mix(in srgb, var(--primary) 9%, var(--surface-2));
|
|
1039
|
+
color: var(--text);
|
|
1040
|
+
border: 1px solid color-mix(in srgb, var(--primary) 55%, var(--line-strong));
|
|
1041
|
+
border-radius: var(--radius-sm);
|
|
1042
|
+
padding: 6px 13px;
|
|
1043
|
+
font-weight: 560;
|
|
1044
|
+
cursor: pointer;
|
|
1045
|
+
transition:
|
|
1046
|
+
background 0.14s ease,
|
|
1047
|
+
border-color 0.14s ease,
|
|
1048
|
+
color 0.14s ease;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
button.btn:hover:not(:disabled) {
|
|
1052
|
+
background: color-mix(in srgb, var(--primary) 18%, var(--surface-2));
|
|
1053
|
+
border-color: var(--primary);
|
|
1054
|
+
color: var(--primary-bright);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
button.btn:disabled {
|
|
1058
|
+
opacity: 0.45;
|
|
1059
|
+
cursor: not-allowed;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
button.btn.primary {
|
|
1063
|
+
background: var(--primary-deep);
|
|
1064
|
+
border-color: var(--primary);
|
|
1065
|
+
color: var(--primary-bright);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
button.btn.primary:hover:not(:disabled) {
|
|
1069
|
+
background: color-mix(in srgb, var(--primary-deep) 60%, var(--primary));
|
|
1070
|
+
color: var(--primary-bright);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
button.btn.danger {
|
|
1074
|
+
color: var(--danger);
|
|
1075
|
+
background: color-mix(in srgb, var(--danger) 8%, var(--surface-2));
|
|
1076
|
+
border-color: color-mix(in srgb, var(--danger) 55%, var(--line-strong));
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
button.btn.danger:hover:not(:disabled) {
|
|
1080
|
+
background: var(--danger-deep);
|
|
1081
|
+
border-color: var(--danger);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/* The quiet ones: cancels, and the per-row Edit in a table of hundreds. */
|
|
1085
|
+
button.btn.ghost {
|
|
1086
|
+
background: none;
|
|
1087
|
+
border-color: transparent;
|
|
1088
|
+
color: var(--text-dim);
|
|
1089
|
+
font-weight: 500;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
button.btn.ghost:hover:not(:disabled) {
|
|
1093
|
+
color: var(--primary-bright);
|
|
1094
|
+
background: var(--surface-2);
|
|
1095
|
+
border-color: var(--line-strong);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
button.btn.small {
|
|
1099
|
+
padding: 4px 10px;
|
|
1100
|
+
font-size: var(--fs-sm);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/* A button's label is one line. In a row that also holds something with
|
|
1104
|
+
* `.grow` in it -- a file picker, a search box, a URL field -- the button
|
|
1105
|
+
* is the item with slack to give, so a two-word label was broken across two
|
|
1106
|
+
* lines and the button came out twice the height of the field beside it.
|
|
1107
|
+
* These rows all wrap between their controls, so a label that really has
|
|
1108
|
+
* nowhere to go takes the next line whole. */
|
|
1109
|
+
.card-foot > button.btn,
|
|
1110
|
+
.actions > button.btn,
|
|
1111
|
+
.toolbar > button.btn {
|
|
1112
|
+
white-space: nowrap;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/* A button whose whole label is one glyph -- a "?", a notification bell,
|
|
1116
|
+
* the live-updates switch. A square target rather than a wide one, so a
|
|
1117
|
+
* lone character does not sit in a button shaped for a sentence. Every one
|
|
1118
|
+
* of these carries its own aria-label: the glyph is the picture, never the
|
|
1119
|
+
* name. */
|
|
1120
|
+
/* One choice in a picker: a device named in the two lines it is named in
|
|
1121
|
+
* everywhere else, read left to right rather than centred like a button
|
|
1122
|
+
* that does something. Taller than a plain button, because a name over a
|
|
1123
|
+
* line about it is two lines; `align-items` keeps whatever the program
|
|
1124
|
+
* files it under, and the badge on the one you are already on, beside the
|
|
1125
|
+
* name rather than beside the second line. */
|
|
1126
|
+
button.btn.pick {
|
|
1127
|
+
display: flex;
|
|
1128
|
+
align-items: center;
|
|
1129
|
+
gap: 10px;
|
|
1130
|
+
width: 100%;
|
|
1131
|
+
padding-top: 7px;
|
|
1132
|
+
padding-bottom: 7px;
|
|
1133
|
+
text-align: left;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/* The name takes the row's slack, so that everything after it ends at the
|
|
1137
|
+
* same place down a list of them and a long model name gives way with an
|
|
1138
|
+
* ellipsis rather than pushing the badge out of the dialog. */
|
|
1139
|
+
button.btn.pick .lines {
|
|
1140
|
+
flex: 1;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/* What a program files a device under that is neither of the two lines --
|
|
1144
|
+
* an address on a bus, the fact that mDNS is what found it. */
|
|
1145
|
+
button.btn.pick .aside {
|
|
1146
|
+
color: var(--text-faint);
|
|
1147
|
+
font-size: var(--fs-xs);
|
|
1148
|
+
font-variant-numeric: tabular-nums;
|
|
1149
|
+
white-space: nowrap;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/* A device that is on the list and did not answer. A dashed edge and dimmed
|
|
1153
|
+
* type, the same way anything absent is drawn on these pages: it is still
|
|
1154
|
+
* there to be picked -- picking it is how you find out what is wrong with it
|
|
1155
|
+
* -- but it is not the same as the ones that answered. */
|
|
1156
|
+
button.btn.pick.gone {
|
|
1157
|
+
border-style: dashed;
|
|
1158
|
+
color: var(--text-dim);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
button.btn.icon {
|
|
1162
|
+
display: inline-flex;
|
|
1163
|
+
align-items: center;
|
|
1164
|
+
justify-content: center;
|
|
1165
|
+
width: 30px;
|
|
1166
|
+
padding: 0;
|
|
1167
|
+
flex: none;
|
|
1168
|
+
aspect-ratio: 1;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
button.btn.icon.small {
|
|
1172
|
+
width: 26px;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/* Live updates. The mark itself changes with the state rather than only
|
|
1176
|
+
* its colour: a flat line while the page is holding still, a pulse while it
|
|
1177
|
+
* is reading. A screen with no colour to read still says which. */
|
|
1178
|
+
button.btn.icon.live {
|
|
1179
|
+
color: var(--text-faint);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
button.btn.icon.live .beat {
|
|
1183
|
+
display: none;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
button.btn.icon.live.on {
|
|
1187
|
+
color: var(--live);
|
|
1188
|
+
border-color: var(--live-deep);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
button.btn.icon.live.on .flat {
|
|
1192
|
+
display: none;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
button.btn.icon.live.on .beat {
|
|
1196
|
+
display: inline;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
button.btn.icon.live:hover:not(:disabled) {
|
|
1200
|
+
color: var(--live);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
input[type="text"],
|
|
1204
|
+
input[type="search"],
|
|
1205
|
+
input[type="number"],
|
|
1206
|
+
input[type="password"],
|
|
1207
|
+
select,
|
|
1208
|
+
textarea {
|
|
1209
|
+
background: var(--surface-2);
|
|
1210
|
+
border: 1px solid var(--line-strong);
|
|
1211
|
+
border-radius: var(--radius-sm);
|
|
1212
|
+
padding: 6px 9px;
|
|
1213
|
+
/* A text input's default width is its `size` -- about 260px -- and that
|
|
1214
|
+
* is an intrinsic size, not a maximum: in a column narrower than that it
|
|
1215
|
+
* is drawn over the card's border and out the other side, and takes the
|
|
1216
|
+
* page's horizontal scrollbar with it. `min-width: 0` lets it shrink
|
|
1217
|
+
* inside a flex or grid track; this is for everywhere else. */
|
|
1218
|
+
max-width: 100%;
|
|
1219
|
+
min-width: 0;
|
|
1220
|
+
transition: border-color 0.14s ease;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
input:focus,
|
|
1224
|
+
select:focus,
|
|
1225
|
+
textarea:focus {
|
|
1226
|
+
border-color: var(--primary);
|
|
1227
|
+
outline: none;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
input::placeholder {
|
|
1231
|
+
color: var(--text-faint);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/* A field holding a draft nobody has applied yet, in the same colour every
|
|
1235
|
+
* other unsent edit on the page wears. */
|
|
1236
|
+
input.pending,
|
|
1237
|
+
select.pending {
|
|
1238
|
+
border-color: var(--live);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/* A row holding an unsent edit says so in its name as well as in its
|
|
1242
|
+
* control. The control's edge is a hairline, and on a switch or a band
|
|
1243
|
+
* there is no edge to colour at all; the name is the part of the row that
|
|
1244
|
+
* is always text, and text in the unsent colour reads at a glance down a
|
|
1245
|
+
* column of twenty. */
|
|
1246
|
+
.row.pending > .k {
|
|
1247
|
+
color: var(--live);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/* A table row holding an unsent edit, the same way: its first cell -- the
|
|
1251
|
+
* name the row is read by -- in the unsent colour. */
|
|
1252
|
+
tr.pending > td:first-child {
|
|
1253
|
+
color: var(--live);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/* A switch that has been flipped and not sent: a ring in the same colour
|
|
1257
|
+
* round its track, the way a dragged setpoint on a band gets one. */
|
|
1258
|
+
label.toggle.pending input[type="checkbox"] {
|
|
1259
|
+
box-shadow: 0 0 0 2px var(--live);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/* A `<select>` sizes itself to its longest *option*, not to its value, and
|
|
1263
|
+
* several of these hold whole sentences. In a card's value column that is
|
|
1264
|
+
* a control wider than the card, drawn straight over the border and out the
|
|
1265
|
+
* other side, and it takes the rows beside it with it. Capped at the
|
|
1266
|
+
* column, the closed box fits and the option that will not is cut with an
|
|
1267
|
+
* ellipsis; the popup a browser opens is not the column's business and is
|
|
1268
|
+
* as wide as it needs to be. */
|
|
1269
|
+
select {
|
|
1270
|
+
max-width: 100%;
|
|
1271
|
+
text-overflow: ellipsis;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/* A file input says two things, and only one of them is ever news.
|
|
1275
|
+
*
|
|
1276
|
+
* "No file selected" is the browser telling you what you already know -- you
|
|
1277
|
+
* have not picked one yet -- in the same colour as every reading on the
|
|
1278
|
+
* page. It is a placeholder, so it is a placeholder's grey; a filename,
|
|
1279
|
+
* once there is one, is the answer to the question the card is asking and
|
|
1280
|
+
* gets the text colour back. */
|
|
1281
|
+
input[type="file"] {
|
|
1282
|
+
color: var(--text-faint);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
input[type="file"].chosen {
|
|
1286
|
+
color: var(--text);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/* The button inside it is a control, not a label, so it is not dimmed with
|
|
1290
|
+
* the sentence beside it -- and while we are in here, it may as well be
|
|
1291
|
+
* this page's button rather than the browser's. */
|
|
1292
|
+
input[type="file"]::file-selector-button {
|
|
1293
|
+
background: color-mix(in srgb, var(--primary) 9%, var(--surface-2));
|
|
1294
|
+
color: var(--text);
|
|
1295
|
+
border: 1px solid color-mix(in srgb, var(--primary) 55%, var(--line-strong));
|
|
1296
|
+
border-radius: var(--radius-sm);
|
|
1297
|
+
padding: 4px 10px;
|
|
1298
|
+
margin-right: 10px;
|
|
1299
|
+
font: inherit;
|
|
1300
|
+
font-size: var(--fs-sm);
|
|
1301
|
+
font-weight: 560;
|
|
1302
|
+
cursor: pointer;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
input[type="file"]:disabled::file-selector-button {
|
|
1306
|
+
opacity: 0.45;
|
|
1307
|
+
cursor: not-allowed;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
input[type="file"]:not(:disabled)::file-selector-button:hover {
|
|
1311
|
+
background: color-mix(in srgb, var(--primary) 18%, var(--surface-2));
|
|
1312
|
+
border-color: var(--primary);
|
|
1313
|
+
color: var(--primary-bright);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
label.toggle {
|
|
1317
|
+
display: inline-flex;
|
|
1318
|
+
align-items: center;
|
|
1319
|
+
gap: 7px;
|
|
1320
|
+
cursor: pointer;
|
|
1321
|
+
user-select: none;
|
|
1322
|
+
font-size: var(--fs-sm);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/* A boolean is a switch, not a tick box.
|
|
1326
|
+
*
|
|
1327
|
+
* These were the browser's own checkboxes, tinted with `accent-color`. A
|
|
1328
|
+
* tick box says "this is one of a list of things you may choose"; every
|
|
1329
|
+
* one of these is a single thing that is either on or off -- live updates,
|
|
1330
|
+
* the balancer, following the log -- and half of them sit in a row of
|
|
1331
|
+
* labels where a small square reads as a bullet. A switch says which of
|
|
1332
|
+
* two states a thing is in from across the room, and says it by position
|
|
1333
|
+
* as well as by colour, which is what a reader who cannot tell the tinted
|
|
1334
|
+
* square from the untinted one needs.
|
|
1335
|
+
*
|
|
1336
|
+
* It is still a checkbox underneath: the same element, the same events,
|
|
1337
|
+
* the same keyboard, the same `:checked`. Nothing that draws one of these
|
|
1338
|
+
* had to change to get it.
|
|
1339
|
+
*/
|
|
1340
|
+
label.toggle input[type="checkbox"] {
|
|
1341
|
+
appearance: none;
|
|
1342
|
+
-webkit-appearance: none;
|
|
1343
|
+
flex: none;
|
|
1344
|
+
margin: 0;
|
|
1345
|
+
/* The track. Sized off the text it sits beside rather than in pixels,
|
|
1346
|
+
* so it is the same height as the label whatever the label is set in. */
|
|
1347
|
+
width: 2.25em;
|
|
1348
|
+
height: 1.25em;
|
|
1349
|
+
border: 1px solid var(--line-strong);
|
|
1350
|
+
border-radius: 999px;
|
|
1351
|
+
background: var(--surface-2);
|
|
1352
|
+
cursor: pointer;
|
|
1353
|
+
position: relative;
|
|
1354
|
+
transition:
|
|
1355
|
+
background 0.16s ease,
|
|
1356
|
+
border-color 0.16s ease;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/* The knob. It travels the width of the track less its own width and the
|
|
1360
|
+
* air either side, which is what makes on and off tell each other apart
|
|
1361
|
+
* without any colour at all.
|
|
1362
|
+
*
|
|
1363
|
+
* It is drawn the way `.band .grip` is -- a ring of the card's own colour
|
|
1364
|
+
* around a grey core, over a hairline that keeps the ring off a background
|
|
1365
|
+
* of the same colour. The two are the same gesture, "take hold of this
|
|
1366
|
+
* and move it", and were drawn two different ways: a plain white disc
|
|
1367
|
+
* here, a ringed one on every band on the page. `box-sizing` is set
|
|
1368
|
+
* rather than inherited, because the `*` rule at the top of this file does
|
|
1369
|
+
* not match pseudo-elements and a border would otherwise grow the knob
|
|
1370
|
+
* past the track.
|
|
1371
|
+
*/
|
|
1372
|
+
label.toggle input[type="checkbox"]::before {
|
|
1373
|
+
content: '';
|
|
1374
|
+
box-sizing: border-box;
|
|
1375
|
+
position: absolute;
|
|
1376
|
+
top: 50%;
|
|
1377
|
+
left: 2px;
|
|
1378
|
+
width: 0.875em;
|
|
1379
|
+
height: 0.875em;
|
|
1380
|
+
margin-top: -0.4375em;
|
|
1381
|
+
border: 2px solid var(--surface);
|
|
1382
|
+
border-radius: 50%;
|
|
1383
|
+
background: var(--text-dim);
|
|
1384
|
+
box-shadow: 0 0 0 1px var(--line-strong);
|
|
1385
|
+
transition:
|
|
1386
|
+
transform 0.16s ease,
|
|
1387
|
+
background 0.16s ease,
|
|
1388
|
+
box-shadow 0.16s ease;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
label.toggle input[type="checkbox"]:checked {
|
|
1392
|
+
background: var(--switch-on, var(--primary));
|
|
1393
|
+
border-color: var(--switch-on, var(--primary));
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/* On, the knob is the same knob: the ring is what tells it from the fill
|
|
1397
|
+
* under it, so it does not have to change colour to stay visible, and the
|
|
1398
|
+
* hairline goes -- against a filled track there is nothing for the ring to
|
|
1399
|
+
* be lost in.
|
|
1400
|
+
*
|
|
1401
|
+
* Which means on and off differ by position and by the colour of the
|
|
1402
|
+
* track, and not by the knob. That is the cost of drawing it like a grip,
|
|
1403
|
+
* and it is why this was a white disc for a while; see the changelog. */
|
|
1404
|
+
label.toggle input[type="checkbox"]:checked::before {
|
|
1405
|
+
box-shadow: none;
|
|
1406
|
+
transform: translateX(1em);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
label.toggle input[type="checkbox"]:hover:not(:disabled) {
|
|
1410
|
+
border-color: var(--switch-on, var(--primary));
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/* Dimmed as a whole -- the switch and the word naming it are one control,
|
|
1414
|
+
* and dimming only the switch reads as a switch that is off. */
|
|
1415
|
+
label.toggle:has(input:disabled) {
|
|
1416
|
+
opacity: 0.45;
|
|
1417
|
+
cursor: not-allowed;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
label.toggle input[type="checkbox"]:disabled {
|
|
1421
|
+
cursor: not-allowed;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1425
|
+
label.toggle input[type="checkbox"],
|
|
1426
|
+
label.toggle input[type="checkbox"]::before {
|
|
1427
|
+
transition: none;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/* Something is happening and there is no honest percentage to put on it.
|
|
1432
|
+
* The page's other answer to that is `.bar.unknown`, which is for a job
|
|
1433
|
+
* with a start and an end; this is for a wait small enough to sit inside a
|
|
1434
|
+
* control.
|
|
1435
|
+
*
|
|
1436
|
+
* Whatever draws one says in words what it is waiting for, so nothing is
|
|
1437
|
+
* carried by the motion alone -- which is what makes the reduced-motion
|
|
1438
|
+
* fallback a ring that has simply stopped, the same answer `.bar.unknown`
|
|
1439
|
+
* gives. */
|
|
1440
|
+
.spinner {
|
|
1441
|
+
display: inline-block;
|
|
1442
|
+
flex: none;
|
|
1443
|
+
box-sizing: border-box;
|
|
1444
|
+
width: 1em;
|
|
1445
|
+
height: 1em;
|
|
1446
|
+
border: 2px solid var(--line-strong);
|
|
1447
|
+
border-top-color: var(--primary);
|
|
1448
|
+
border-radius: 50%;
|
|
1449
|
+
animation: turn 0.7s linear infinite;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
@keyframes turn {
|
|
1453
|
+
to {
|
|
1454
|
+
transform: rotate(360deg);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1459
|
+
.spinner {
|
|
1460
|
+
animation: none;
|
|
1461
|
+
opacity: 0.55;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/* See `--slider-lift`: the light theme's own widget shadow, and a faint
|
|
1466
|
+
* light rim in its place on a page too dark for a shadow to show on. */
|
|
1467
|
+
input[type="range"] {
|
|
1468
|
+
accent-color: var(--primary);
|
|
1469
|
+
filter: var(--slider-lift);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/* One shape wherever a control and its reading appear together. */
|
|
1473
|
+
.set {
|
|
1474
|
+
display: flex;
|
|
1475
|
+
align-items: center;
|
|
1476
|
+
flex-wrap: wrap;
|
|
1477
|
+
gap: 10px;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
.set input[type="range"] {
|
|
1481
|
+
flex: 1 1 110px;
|
|
1482
|
+
min-width: 110px;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
.val {
|
|
1486
|
+
font-variant-numeric: tabular-nums;
|
|
1487
|
+
font-weight: 560;
|
|
1488
|
+
flex: none;
|
|
1489
|
+
min-width: 4.6em;
|
|
1490
|
+
text-align: right;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
.val.pending {
|
|
1494
|
+
color: var(--live);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/* A typed number and the unit it is in.
|
|
1498
|
+
*
|
|
1499
|
+
* A unit is part of the number, so it sits against it; a column is kept, at
|
|
1500
|
+
* the width of a unit rather than of a reading, so every input on a card
|
|
1501
|
+
* still ends at the same place. And it is read from its right-hand edge,
|
|
1502
|
+
* like every other value in the card. */
|
|
1503
|
+
.set.num {
|
|
1504
|
+
gap: 7px;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
.set.num > input[type="number"] {
|
|
1508
|
+
width: 90px;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
.unit {
|
|
1512
|
+
flex: none;
|
|
1513
|
+
min-width: 2.2em;
|
|
1514
|
+
text-align: right;
|
|
1515
|
+
color: var(--text-dim);
|
|
1516
|
+
font-size: var(--fs-sm);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
.unit.pending {
|
|
1520
|
+
color: var(--live);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/* The shapes a panel arranges controls in, so they are one shape rather
|
|
1524
|
+
* than forty inline `style="display:flex;gap:10px"` attributes that each
|
|
1525
|
+
* drifted a pixel or two from the others. */
|
|
1526
|
+
.actions {
|
|
1527
|
+
display: flex;
|
|
1528
|
+
gap: 10px;
|
|
1529
|
+
align-items: center;
|
|
1530
|
+
flex-wrap: wrap;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
.actions.centred {
|
|
1534
|
+
justify-content: center;
|
|
1535
|
+
margin-top: 12px;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/* A row of controls that follows something else in a card keeps its
|
|
1539
|
+
* distance from it, so the gap is the card's and not each caller's guess. */
|
|
1540
|
+
.card-body > .actions:not(:first-child) {
|
|
1541
|
+
margin-top: 12px;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
.toolbar {
|
|
1545
|
+
display: flex;
|
|
1546
|
+
gap: 10px;
|
|
1547
|
+
align-items: center;
|
|
1548
|
+
flex-wrap: wrap;
|
|
1549
|
+
margin-bottom: 14px;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
.stack {
|
|
1553
|
+
display: grid;
|
|
1554
|
+
gap: 6px;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
.wrap {
|
|
1558
|
+
display: flex;
|
|
1559
|
+
align-items: center;
|
|
1560
|
+
gap: 8px;
|
|
1561
|
+
flex-wrap: wrap;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/* --- a list of things that arrived from outside ---------------------------
|
|
1565
|
+
*
|
|
1566
|
+
* Rows of one identifier and a line about it: the browsers watching this
|
|
1567
|
+
* server, the firmware images on the vendor's, the networks a radio found.
|
|
1568
|
+
* One shape, because they are one kind of list -- something out there, and
|
|
1569
|
+
* what is known about it.
|
|
1570
|
+
*
|
|
1571
|
+
* `you` is the row that is this browser, in the watcher list `Watchers`
|
|
1572
|
+
* draws: the whole question that list answers is whether the other watcher
|
|
1573
|
+
* is me, so the answer is on the row rather than only in the words. */
|
|
1574
|
+
.entry {
|
|
1575
|
+
display: grid;
|
|
1576
|
+
gap: 3px;
|
|
1577
|
+
padding: 11px 13px;
|
|
1578
|
+
border: 1px solid var(--line);
|
|
1579
|
+
border-radius: var(--radius-sm);
|
|
1580
|
+
background: var(--surface-2);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
.entry + .entry { margin-top: 8px; }
|
|
1584
|
+
.entry .head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
|
|
1585
|
+
.entry .name { font-weight: 560; }
|
|
1586
|
+
.entry .name.data {
|
|
1587
|
+
font-family: var(--mono);
|
|
1588
|
+
font-size: var(--fs-sm);
|
|
1589
|
+
font-weight: 500;
|
|
1590
|
+
overflow-wrap: anywhere;
|
|
1591
|
+
}
|
|
1592
|
+
.entry .meta { color: var(--text-dim); font-size: var(--fs-sm); }
|
|
1593
|
+
.entry .act { margin-left: auto; }
|
|
1594
|
+
.entry.you { border-color: var(--primary-deep); }
|
|
1595
|
+
|
|
1596
|
+
.scroller {
|
|
1597
|
+
max-height: min(52vh, 460px);
|
|
1598
|
+
overflow-y: auto;
|
|
1599
|
+
padding-right: 2px;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
/* --- progress -------------------------------------------------------------- */
|
|
1603
|
+
|
|
1604
|
+
/* A read that is taking long enough to be worth a bar. Sits between a
|
|
1605
|
+
* panel's toolbar and its content, so the thing you are waiting for is
|
|
1606
|
+
* where the answer will appear. */
|
|
1607
|
+
.reading {
|
|
1608
|
+
margin-bottom: 14px;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
.reading .what {
|
|
1612
|
+
display: flex;
|
|
1613
|
+
align-items: baseline;
|
|
1614
|
+
gap: 12px;
|
|
1615
|
+
font-size: var(--fs-sm);
|
|
1616
|
+
color: var(--text-dim);
|
|
1617
|
+
margin-bottom: 6px;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
.reading .what .muted {
|
|
1621
|
+
margin-left: auto;
|
|
1622
|
+
font-variant-numeric: tabular-nums;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
.bar {
|
|
1626
|
+
height: 6px;
|
|
1627
|
+
border-radius: 999px;
|
|
1628
|
+
background: var(--surface-3);
|
|
1629
|
+
overflow: hidden;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
.bar > span {
|
|
1633
|
+
display: block;
|
|
1634
|
+
height: 100%;
|
|
1635
|
+
border-radius: 999px;
|
|
1636
|
+
background: linear-gradient(90deg, var(--bar-from), var(--bar-to));
|
|
1637
|
+
transition: width 0.2s ease;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
.bar-note {
|
|
1641
|
+
font-size: var(--fs-xs);
|
|
1642
|
+
margin-top: 4px;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/* Nothing knows how many pages a device's log has, so a bar that claimed a
|
|
1646
|
+
* percentage would be making one up. It sweeps instead: motion says "still
|
|
1647
|
+
* going", which is the whole question being asked. */
|
|
1648
|
+
.bar.unknown > span {
|
|
1649
|
+
width: 35%;
|
|
1650
|
+
animation: sweep 1.4s ease-in-out infinite;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
@keyframes sweep {
|
|
1654
|
+
0% {
|
|
1655
|
+
margin-left: 0;
|
|
1656
|
+
}
|
|
1657
|
+
50% {
|
|
1658
|
+
margin-left: 65%;
|
|
1659
|
+
}
|
|
1660
|
+
100% {
|
|
1661
|
+
margin-left: 0;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1666
|
+
.bar.unknown > span {
|
|
1667
|
+
animation: none;
|
|
1668
|
+
width: 100%;
|
|
1669
|
+
opacity: 0.55;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/* What a card looks like on its way in: the shape of the rows that are
|
|
1674
|
+
* coming, so the grid is already the right size when they land. */
|
|
1675
|
+
.skeleton {
|
|
1676
|
+
display: grid;
|
|
1677
|
+
gap: 12px;
|
|
1678
|
+
padding: 4px 0;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
.skeleton .bone {
|
|
1682
|
+
height: 11px;
|
|
1683
|
+
border-radius: 999px;
|
|
1684
|
+
background: linear-gradient(
|
|
1685
|
+
90deg,
|
|
1686
|
+
var(--surface-2),
|
|
1687
|
+
var(--surface-3),
|
|
1688
|
+
var(--surface-2)
|
|
1689
|
+
);
|
|
1690
|
+
background-size: 300% 100%;
|
|
1691
|
+
animation: shimmer 1.6s ease-in-out infinite;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
.skeleton .bone:nth-child(2n) {
|
|
1695
|
+
width: 82%;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
.skeleton .bone:nth-child(3n) {
|
|
1699
|
+
width: 64%;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
@keyframes shimmer {
|
|
1703
|
+
0% {
|
|
1704
|
+
background-position: 100% 0;
|
|
1705
|
+
}
|
|
1706
|
+
100% {
|
|
1707
|
+
background-position: -100% 0;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1712
|
+
.skeleton .bone {
|
|
1713
|
+
animation: none;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/* --- the server has gone ---------------------------------------------------- */
|
|
1718
|
+
|
|
1719
|
+
/* This was a bar of its own between the header and the tabs, which meant
|
|
1720
|
+
* every blip of the server moved the whole page down and back. It rides in
|
|
1721
|
+
* the header's slack instead: `min-width: 0` and an ellipsis so it gives the
|
|
1722
|
+
* space back rather than pushing the pill along, and no height of its own --
|
|
1723
|
+
* the header is already taller than one line of this text. See
|
|
1724
|
+
* `OfflineNotice` in js/ui.js for why the sentence is hidden from the reader
|
|
1725
|
+
* here and repeated in `.sr-only`. */
|
|
1726
|
+
.offline-note {
|
|
1727
|
+
display: flex;
|
|
1728
|
+
align-items: center;
|
|
1729
|
+
gap: 8px;
|
|
1730
|
+
min-width: 0;
|
|
1731
|
+
font-size: var(--fs-sm);
|
|
1732
|
+
color: var(--warn);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
.offline-note.gone {
|
|
1736
|
+
color: var(--danger);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
.offline-note .dot {
|
|
1740
|
+
width: 7px;
|
|
1741
|
+
height: 7px;
|
|
1742
|
+
border-radius: 50%;
|
|
1743
|
+
background: currentColor;
|
|
1744
|
+
flex: none;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
.offline-note .what {
|
|
1748
|
+
min-width: 0;
|
|
1749
|
+
overflow: hidden;
|
|
1750
|
+
text-overflow: ellipsis;
|
|
1751
|
+
white-space: nowrap;
|
|
1752
|
+
color: var(--text);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/* The sentence is the only part that gives: the way out of this is two
|
|
1756
|
+
* words on a button, and "Try / now" on two lines in a header that is
|
|
1757
|
+
* already the wrong shape is not a way out anybody reads. */
|
|
1758
|
+
.offline-note .btn {
|
|
1759
|
+
flex: none;
|
|
1760
|
+
white-space: nowrap;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/* On a phone there is no slack to ride in: the dot and the button stay and
|
|
1764
|
+
* the sentence goes, which costs nothing, because the whole of it is in the
|
|
1765
|
+
* `.sr-only` copy beside it either way. Eliding text for the eye must not
|
|
1766
|
+
* elide it for a reader. */
|
|
1767
|
+
@media (max-width: 760px) {
|
|
1768
|
+
header.top .offline-note .what {
|
|
1769
|
+
display: none;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
/* --- dialogs and toasts ------------------------------------------------------ */
|
|
1774
|
+
|
|
1775
|
+
.backdrop {
|
|
1776
|
+
position: fixed;
|
|
1777
|
+
inset: 0;
|
|
1778
|
+
z-index: 60;
|
|
1779
|
+
background: rgb(0 0 0 / 72%);
|
|
1780
|
+
display: grid;
|
|
1781
|
+
place-items: center;
|
|
1782
|
+
padding: 20px;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
.modal {
|
|
1786
|
+
background: var(--surface);
|
|
1787
|
+
border: 1px solid var(--line-strong);
|
|
1788
|
+
border-radius: var(--radius);
|
|
1789
|
+
padding: 20px 22px;
|
|
1790
|
+
width: min(var(--modal-width, 460px), 100%);
|
|
1791
|
+
display: grid;
|
|
1792
|
+
gap: 14px;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
.modal h3 {
|
|
1796
|
+
margin: 0;
|
|
1797
|
+
font-size: var(--fs-lg);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
.modal p {
|
|
1801
|
+
margin: 0;
|
|
1802
|
+
color: var(--text-dim);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
.modal .buttons {
|
|
1806
|
+
display: flex;
|
|
1807
|
+
justify-content: flex-end;
|
|
1808
|
+
gap: 8px;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
.modal .buttons:empty {
|
|
1812
|
+
display: none;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
/* The word that has to be typed before the button will do anything. */
|
|
1816
|
+
.modal input.typed {
|
|
1817
|
+
margin-left: 8px;
|
|
1818
|
+
width: 8rem;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
.toasts {
|
|
1822
|
+
position: fixed;
|
|
1823
|
+
right: 18px;
|
|
1824
|
+
bottom: 18px;
|
|
1825
|
+
z-index: 80;
|
|
1826
|
+
display: grid;
|
|
1827
|
+
gap: 8px;
|
|
1828
|
+
width: min(400px, calc(100vw - 36px));
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
.toast {
|
|
1832
|
+
border: 1px solid var(--line-strong);
|
|
1833
|
+
border-left-width: 3px;
|
|
1834
|
+
border-radius: var(--radius-sm);
|
|
1835
|
+
background: var(--surface-2);
|
|
1836
|
+
padding: 11px 13px;
|
|
1837
|
+
font-size: var(--toast-size);
|
|
1838
|
+
display: flex;
|
|
1839
|
+
gap: 10px;
|
|
1840
|
+
align-items: flex-start;
|
|
1841
|
+
animation: slide-in 0.16s ease-out;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
.toast.error {
|
|
1845
|
+
border-left-color: var(--danger);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
.toast.ok {
|
|
1849
|
+
border-left-color: var(--ok);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
.toast.info {
|
|
1853
|
+
border-left-color: var(--primary);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/* The sentence, and under it whatever can be done about it right there --
|
|
1857
|
+
* fetching the recording a failure started, stopping it. */
|
|
1858
|
+
.toast .said {
|
|
1859
|
+
display: grid;
|
|
1860
|
+
gap: 9px;
|
|
1861
|
+
min-width: 0;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
.toast-actions {
|
|
1865
|
+
display: flex;
|
|
1866
|
+
flex-wrap: wrap;
|
|
1867
|
+
gap: 6px;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
.toast .x {
|
|
1871
|
+
margin-left: auto;
|
|
1872
|
+
cursor: pointer;
|
|
1873
|
+
color: var(--text-faint);
|
|
1874
|
+
background: none;
|
|
1875
|
+
border: 0;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
@keyframes slide-in {
|
|
1879
|
+
from {
|
|
1880
|
+
opacity: 0;
|
|
1881
|
+
transform: translateY(6px);
|
|
1882
|
+
}
|
|
1883
|
+
to {
|
|
1884
|
+
opacity: 1;
|
|
1885
|
+
transform: none;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1890
|
+
.toast {
|
|
1891
|
+
animation: none;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
|
|
1896
|
+
/* --- the page's own frame -------------------------------------------------
|
|
1897
|
+
*
|
|
1898
|
+
* The header, the tab strip, the pane that holds a tab, and the licence
|
|
1899
|
+
* line under all of it. These are drawn by `js/shell.js`, which every
|
|
1900
|
+
* program of this shape had re-typed for itself, so the rules move with
|
|
1901
|
+
* it. What stays behind is what a program puts *in* the header -- its
|
|
1902
|
+
* own wordmark, and whatever it calls the thing it is talking to.
|
|
1903
|
+
*/
|
|
1904
|
+
|
|
1905
|
+
/* A column: the header, whatever is warning you, and the page.
|
|
1906
|
+
*
|
|
1907
|
+
* This was `grid-template-rows: auto 1fr`, which was right for exactly two
|
|
1908
|
+
* children and wrong the moment the offline banner became a third -- the
|
|
1909
|
+
* banner would have taken the `1fr` and the page the leftover `auto`, so
|
|
1910
|
+
* the one row that says the server has gone would have been the tallest
|
|
1911
|
+
* thing on the screen. */
|
|
1912
|
+
.app {
|
|
1913
|
+
display: flex;
|
|
1914
|
+
flex-direction: column;
|
|
1915
|
+
min-height: 100vh;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
/* And a column again inside, so the licence line below the tabs is a
|
|
1919
|
+
* *footer*: on a short tab -- a picker, an empty state, one card -- it was
|
|
1920
|
+
* drawn immediately under the content and left half the window blank below
|
|
1921
|
+
* it, which reads as the page having stopped rather than as a footer. The
|
|
1922
|
+
* main region takes the slack instead and the line lands on the bottom
|
|
1923
|
+
* edge, wherever the content ends. */
|
|
1924
|
+
.app > .body {
|
|
1925
|
+
flex: 1 1 auto;
|
|
1926
|
+
display: flex;
|
|
1927
|
+
flex-direction: column;
|
|
1928
|
+
min-height: 0;
|
|
1929
|
+
}
|
|
1930
|
+
.app > .body > main { flex: 1 1 auto; }
|
|
1931
|
+
.app > .body > .license { margin-top: auto; }
|
|
1932
|
+
|
|
1933
|
+
/* The header is two rows -- what the page is about, and the tabs -- and
|
|
1934
|
+
* neither of them is the header itself: both are centred on the content
|
|
1935
|
+
* column inside it, so the wordmark, the first tab and the first card share
|
|
1936
|
+
* one left edge however wide the window is.
|
|
1937
|
+
*
|
|
1938
|
+
* One program had this and the other did not, which on a wide screen meant
|
|
1939
|
+
* a wordmark against the far left of the window with its own page floating
|
|
1940
|
+
* in the middle. `Header` in js/shell.js draws both rows now, so neither
|
|
1941
|
+
* program decides this for itself. */
|
|
1942
|
+
header.top {
|
|
1943
|
+
position: sticky;
|
|
1944
|
+
top: 0;
|
|
1945
|
+
z-index: 20;
|
|
1946
|
+
display: block;
|
|
1947
|
+
padding: 0;
|
|
1948
|
+
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
|
1949
|
+
backdrop-filter: blur(8px);
|
|
1950
|
+
border-bottom: 1px solid var(--line);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
/* One row, and it shrinks rather than wraps.
|
|
1954
|
+
*
|
|
1955
|
+
* The offline line rides in the slack here and gives the space back with an
|
|
1956
|
+
* ellipsis when there is none -- which only works if the row refuses to
|
|
1957
|
+
* wrap, or the widest thing on it pushes the theme switch onto a second row
|
|
1958
|
+
* and the whole page steps down every time the server blinks. Below the
|
|
1959
|
+
* phone breakpoint there is no slack to ride in, so it wraps there and the
|
|
1960
|
+
* sentence goes (it stays in full in the `.sr-only` copy beside it). */
|
|
1961
|
+
.top-inner {
|
|
1962
|
+
max-width: var(--page);
|
|
1963
|
+
margin: 0 auto;
|
|
1964
|
+
padding: 10px var(--gutter);
|
|
1965
|
+
display: flex;
|
|
1966
|
+
align-items: center;
|
|
1967
|
+
gap: 12px;
|
|
1968
|
+
flex-wrap: nowrap;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
@media (max-width: 760px) {
|
|
1972
|
+
.top-inner {
|
|
1973
|
+
flex-wrap: wrap;
|
|
1974
|
+
row-gap: 10px;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
.top-inner .device-id {
|
|
1978
|
+
order: 3;
|
|
1979
|
+
flex: 1 1 100%;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/* --- a page's unsent edits, where they can always be seen -----------------
|
|
1984
|
+
*
|
|
1985
|
+
* `PageApply` in js/panels.js. A page that is one draft across a dozen
|
|
1986
|
+
* cards had its Apply above the first of them, which scrolls away long
|
|
1987
|
+
* before the setting at the bottom is reached; the header is pinned, so it
|
|
1988
|
+
* is the one place on the page that is always in view.
|
|
1989
|
+
*
|
|
1990
|
+
* It rides in the header's slack, the way the offline line does. It goes
|
|
1991
|
+
* after the spacer, so everything to its right -- the pill, the switches --
|
|
1992
|
+
* stays exactly where it was, and everything to its left is not reached
|
|
1993
|
+
* until the spacer has given up all of its width. It is no taller than
|
|
1994
|
+
* the two lines naming the device, which set the header's height, so the
|
|
1995
|
+
* header does not grow by a pixel when it appears. And the sentence is the
|
|
1996
|
+
* part that gives way when the row runs short, not the buttons: "Review 3
|
|
1997
|
+
* changes" cut to "Rev..." is not a way to send anything.
|
|
1998
|
+
*/
|
|
1999
|
+
.page-apply {
|
|
2000
|
+
display: flex;
|
|
2001
|
+
align-items: center;
|
|
2002
|
+
gap: 8px;
|
|
2003
|
+
flex: 0 1 auto;
|
|
2004
|
+
min-width: 0;
|
|
2005
|
+
/* No edge and no padding above or below: the buttons are the height, and
|
|
2006
|
+
* a small button is shorter than the link pill beside it, which is
|
|
2007
|
+
* shorter than the name. A border round it made it three pixels taller
|
|
2008
|
+
* than the name and the whole header grew by three when it appeared. */
|
|
2009
|
+
padding: 0 0 0 10px;
|
|
2010
|
+
border-radius: var(--radius-sm);
|
|
2011
|
+
background: color-mix(in srgb, var(--live) 14%, transparent);
|
|
2012
|
+
box-shadow: inset 3px 0 0 var(--live);
|
|
2013
|
+
font-size: var(--fs-sm);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
.page-apply .dot {
|
|
2017
|
+
flex: none;
|
|
2018
|
+
width: 7px;
|
|
2019
|
+
height: 7px;
|
|
2020
|
+
border-radius: 50%;
|
|
2021
|
+
background: var(--live);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
.page-apply .state {
|
|
2025
|
+
min-width: 0;
|
|
2026
|
+
overflow: hidden;
|
|
2027
|
+
text-overflow: ellipsis;
|
|
2028
|
+
white-space: nowrap;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
.page-apply .btn {
|
|
2032
|
+
flex: none;
|
|
2033
|
+
white-space: nowrap;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
/* On a phone the header is two rows -- the wordmark and the switches, then
|
|
2037
|
+
* the device's name across the whole width -- and there is no slack in the
|
|
2038
|
+
* first. So the group is laid over the right-hand end of the second, and
|
|
2039
|
+
* the name gives up that much width and ellipsises, as it does anyway when
|
|
2040
|
+
* it is long. Out of the flow, so the rows are exactly as tall as they
|
|
2041
|
+
* were. The sentence goes -- the group is labelled with it in full for a
|
|
2042
|
+
* reader -- and the dot and the two buttons stay. */
|
|
2043
|
+
@media (max-width: 760px) {
|
|
2044
|
+
.top-inner {
|
|
2045
|
+
position: relative;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
.page-apply {
|
|
2049
|
+
position: absolute;
|
|
2050
|
+
right: var(--gutter);
|
|
2051
|
+
bottom: 12px;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/* Padded rather than narrowed: the name keeps its whole row, so nothing
|
|
2055
|
+
* from the row above can wrap down beside it when the group appears. A
|
|
2056
|
+
* narrower name let a switch that had wrapped onto a row of its own move
|
|
2057
|
+
* up next to it, and the header lost a row -- and the page jumped up by
|
|
2058
|
+
* one -- with the first edit. */
|
|
2059
|
+
.top-inner:has(.page-apply) .device-id {
|
|
2060
|
+
box-sizing: border-box;
|
|
2061
|
+
padding-right: 270px;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
.page-apply .state {
|
|
2065
|
+
display: none;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
/* The one edge the page already draws, tinted, rather than a new one:
|
|
2070
|
+
* a colour change on an existing hairline reflows nothing. */
|
|
2071
|
+
header.top.lost { border-bottom-color: var(--live-deep); }
|
|
2072
|
+
header.top.gone { border-bottom-color: var(--danger-deep); }
|
|
2073
|
+
|
|
2074
|
+
/* --- the wordmark ----------------------------------------------------------
|
|
2075
|
+
*
|
|
2076
|
+
* Two links in one mark: the name goes to the project's page, the version
|
|
2077
|
+
* beside it to the release notes for exactly this build. Neither URL is
|
|
2078
|
+
* written into the page -- both come from the program's own packaging
|
|
2079
|
+
* metadata by way of `GET /api/about`, which is the only place either is
|
|
2080
|
+
* written down at all. See `Brand` in js/shell.js.
|
|
2081
|
+
*
|
|
2082
|
+
* The glyph in front of the name is the one part that is a program's own,
|
|
2083
|
+
* and it is passed in rather than styled here.
|
|
2084
|
+
*/
|
|
2085
|
+
/* Centred, not on a baseline.
|
|
2086
|
+
*
|
|
2087
|
+
* Baseline was right while every program's mark was a character -- a bolt,
|
|
2088
|
+
* a plug -- because a character sits on the same baseline as the word after
|
|
2089
|
+
* it. One of these marks is *drawn*: a battery, two divs and a border, with
|
|
2090
|
+
* no baseline of its own. A flex container takes its baseline from its
|
|
2091
|
+
* first item, so `.home` handed the row the bottom edge of that battery,
|
|
2092
|
+
* the row put that edge on the baseline, and the program's name was lifted
|
|
2093
|
+
* several pixels above the version sitting next to it.
|
|
2094
|
+
*
|
|
2095
|
+
* Centring asks nothing of what the mark is made of, which is the only
|
|
2096
|
+
* thing a shared header can safely assume about it. */
|
|
2097
|
+
.brand {
|
|
2098
|
+
display: flex;
|
|
2099
|
+
align-items: center;
|
|
2100
|
+
gap: 8px;
|
|
2101
|
+
flex: none;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
.brand .home {
|
|
2105
|
+
display: inline-flex;
|
|
2106
|
+
align-items: center;
|
|
2107
|
+
gap: 8px;
|
|
2108
|
+
text-decoration: none;
|
|
2109
|
+
color: inherit;
|
|
2110
|
+
line-height: 1.2;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
a.home { cursor: pointer; }
|
|
2114
|
+
|
|
2115
|
+
.brand .name {
|
|
2116
|
+
font-weight: 620;
|
|
2117
|
+
letter-spacing: -0.01em;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
/* The version reads as an annotation on the name rather than a link of
|
|
2121
|
+
* equal standing, so it is faint and lighter -- but it still answers the
|
|
2122
|
+
* pointer, and says where it goes. */
|
|
2123
|
+
.brand .ver {
|
|
2124
|
+
color: var(--text-faint);
|
|
2125
|
+
font-weight: 500;
|
|
2126
|
+
font-size: var(--fs-sm);
|
|
2127
|
+
text-decoration: none;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
.brand .home:hover .name,
|
|
2131
|
+
.brand .ver:hover {
|
|
2132
|
+
color: var(--brand-hover, var(--primary-bright));
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
/* --- which device this page is about ---------------------------------------
|
|
2136
|
+
*
|
|
2137
|
+
* Two lines, in the header, on every tab: what this one is called -- a name
|
|
2138
|
+
* it was given, or a serial, something no other device on the bench shares
|
|
2139
|
+
* -- and under it what it is, which is the model and the versions of the
|
|
2140
|
+
* hardware and the software answering.
|
|
2141
|
+
*
|
|
2142
|
+
* Both halves are why: a reading is about *that* device, and what a device
|
|
2143
|
+
* will answer at all depends on which firmware is on it. A page that made
|
|
2144
|
+
* you open a tab to find either is a page that lets you read the right
|
|
2145
|
+
* number off the wrong box.
|
|
2146
|
+
*/
|
|
2147
|
+
.device-id {
|
|
2148
|
+
display: flex;
|
|
2149
|
+
align-items: center;
|
|
2150
|
+
gap: 6px;
|
|
2151
|
+
min-width: 0;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
/* The two lines a device is named by, wherever it is named: in the header,
|
|
2155
|
+
* and in the dialog that changes which device the page is about. See
|
|
2156
|
+
* `Lines` in js/ui.js. */
|
|
2157
|
+
.lines {
|
|
2158
|
+
display: flex;
|
|
2159
|
+
flex-direction: column;
|
|
2160
|
+
line-height: 1.25;
|
|
2161
|
+
min-width: 0;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
.lines .primary {
|
|
2165
|
+
font-weight: 560;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
.lines .secondary {
|
|
2169
|
+
color: var(--text-dim);
|
|
2170
|
+
font-size: var(--fs-sm);
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
/* Neither line wraps to a second one: in the header this block is what sets
|
|
2174
|
+
* the height, and a long model name pushing it to four lines moves the
|
|
2175
|
+
* whole page down. `min-width: 0` above is what lets them give way
|
|
2176
|
+
* instead. */
|
|
2177
|
+
.lines .primary,
|
|
2178
|
+
.lines .secondary {
|
|
2179
|
+
overflow: hidden;
|
|
2180
|
+
text-overflow: ellipsis;
|
|
2181
|
+
white-space: nowrap;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
/* The name as the way to change device.
|
|
2185
|
+
*
|
|
2186
|
+
* There was a ghost button reading "change" beside it, which is a second
|
|
2187
|
+
* control for an action whose subject is the thing next to it; the name is
|
|
2188
|
+
* what you read to realise you are on the wrong device, so the name is what
|
|
2189
|
+
* you go to click.
|
|
2190
|
+
*
|
|
2191
|
+
* Under the pointer it becomes a button, in the same fill, edge and colour
|
|
2192
|
+
* as every other button on the page: it had only a faint grey wash, which
|
|
2193
|
+
* on a header that is already a panel is barely a change at all, so the one
|
|
2194
|
+
* control on the page that is not shaped like a button also did not behave
|
|
2195
|
+
* like one on hover. The border is there the whole time and transparent
|
|
2196
|
+
* until then, and the padding is a pixel smaller than the border is wide
|
|
2197
|
+
* -- 2px + 1px against the 3px this used to have -- so nothing under the
|
|
2198
|
+
* header moves when the edge appears.
|
|
2199
|
+
*
|
|
2200
|
+
* The padding is taken straight back out again as a negative margin. The
|
|
2201
|
+
* header is exactly as tall as this block, and a control needs room around
|
|
2202
|
+
* its text for a hover to look like anything; without the negative margin
|
|
2203
|
+
* that room would be six more pixels of header on every page, and the tab
|
|
2204
|
+
* strip and everything under it would sit six pixels lower than it does in
|
|
2205
|
+
* the program next to it. Margin does not change how tall a flex item's
|
|
2206
|
+
* outer box is, so this one is still the height of two lines of text.
|
|
2207
|
+
*/
|
|
2208
|
+
.device-id.pick {
|
|
2209
|
+
padding: 2px 5px;
|
|
2210
|
+
margin: -3px -6px;
|
|
2211
|
+
border: 1px solid transparent;
|
|
2212
|
+
border-radius: var(--radius-sm);
|
|
2213
|
+
background: none;
|
|
2214
|
+
color: inherit;
|
|
2215
|
+
font: inherit;
|
|
2216
|
+
text-align: left;
|
|
2217
|
+
cursor: pointer;
|
|
2218
|
+
transition:
|
|
2219
|
+
background 0.14s ease,
|
|
2220
|
+
border-color 0.14s ease,
|
|
2221
|
+
color 0.14s ease;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
/* The same fill and edge `button.btn:hover` wears. Only the first line
|
|
2225
|
+
* takes the button's text colour: the second is the model and the firmware
|
|
2226
|
+
* versions, which is the caption to the name and stays the caption. */
|
|
2227
|
+
.device-id.pick:hover,
|
|
2228
|
+
.device-id.pick:focus-visible {
|
|
2229
|
+
background: color-mix(in srgb, var(--primary) 18%, var(--surface-2));
|
|
2230
|
+
border-color: var(--primary);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
.device-id.pick:hover .primary,
|
|
2234
|
+
.device-id.pick:focus-visible .primary {
|
|
2235
|
+
color: var(--primary-bright);
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2239
|
+
.device-id.pick {
|
|
2240
|
+
transition: none;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/* The program's own mark, in front of its name. Its size is set where it
|
|
2245
|
+
* is drawn; what is here is only that it may not be squeezed by a long
|
|
2246
|
+
* device name sharing the row. */
|
|
2247
|
+
.brand .glyph {
|
|
2248
|
+
flex: none;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
/* --- what the link is doing, as one control --------------------------------
|
|
2252
|
+
*
|
|
2253
|
+
* Both programs hang the same thing off the right of the header: a pill
|
|
2254
|
+
* saying what the connection to the device is doing, which opens a menu
|
|
2255
|
+
* holding everything else about it. They had drawn it twice -- one a
|
|
2256
|
+
* bordered capsule that tints, the other a capsule that recolours its own
|
|
2257
|
+
* text -- so the two headers read as two different programs at the one
|
|
2258
|
+
* glance a header is for. What is left to each is the words and what goes
|
|
2259
|
+
* in the menu.
|
|
2260
|
+
*/
|
|
2261
|
+
.pill {
|
|
2262
|
+
display: inline-flex;
|
|
2263
|
+
align-items: center;
|
|
2264
|
+
gap: 8px;
|
|
2265
|
+
padding: 5px 11px 5px 9px;
|
|
2266
|
+
border: 1px solid var(--line-strong);
|
|
2267
|
+
border-radius: 999px;
|
|
2268
|
+
background: var(--surface);
|
|
2269
|
+
font-size: var(--fs-sm);
|
|
2270
|
+
/* Two or three words, on one line. This sits at the tight end of a
|
|
2271
|
+
* header that shrinks, and a pill reading "port / held" is the header
|
|
2272
|
+
* having run out of room rather than the port having done anything. */
|
|
2273
|
+
flex: none;
|
|
2274
|
+
white-space: nowrap;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
button.pill {
|
|
2278
|
+
cursor: pointer;
|
|
2279
|
+
text-align: left;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
button.pill:hover,
|
|
2283
|
+
.anchor.open > .pill {
|
|
2284
|
+
border-color: var(--line-strong);
|
|
2285
|
+
background: var(--surface-2);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
.pill .dot {
|
|
2289
|
+
width: 7px;
|
|
2290
|
+
height: 7px;
|
|
2291
|
+
border-radius: 50%;
|
|
2292
|
+
background: var(--text-faint);
|
|
2293
|
+
flex: none;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
/* The label is the state's name and never abbreviates; what gives when the
|
|
2297
|
+
* pill runs out of room is the operation text beside it. */
|
|
2298
|
+
.pill .label {
|
|
2299
|
+
color: var(--text-dim);
|
|
2300
|
+
flex: none;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
.pill-op {
|
|
2304
|
+
color: var(--text);
|
|
2305
|
+
overflow: hidden;
|
|
2306
|
+
text-overflow: ellipsis;
|
|
2307
|
+
white-space: nowrap;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
.pill .chev {
|
|
2311
|
+
color: var(--text-faint);
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
.pill .queued {
|
|
2315
|
+
font-size: var(--fs-xs);
|
|
2316
|
+
color: var(--live);
|
|
2317
|
+
border-left: 1px solid var(--line-strong);
|
|
2318
|
+
padding-left: 8px;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
.pill.released {
|
|
2322
|
+
border-color: var(--line);
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
.pill.idle {
|
|
2326
|
+
border-color: var(--primary-deep);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
.pill.idle .dot {
|
|
2330
|
+
background: var(--primary);
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
.pill.opening {
|
|
2334
|
+
border-color: var(--live-deep);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
.pill.busy {
|
|
2338
|
+
border-color: var(--live-deep);
|
|
2339
|
+
background: color-mix(in srgb, var(--live-deep) 30%, var(--surface));
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
.pill.busy .dot,
|
|
2343
|
+
.pill.opening .dot,
|
|
2344
|
+
.pill.flashing .dot {
|
|
2345
|
+
background: var(--live);
|
|
2346
|
+
animation: throb 1.1s ease-in-out infinite;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
.pill.busy .label,
|
|
2350
|
+
.pill.opening .label {
|
|
2351
|
+
color: var(--live);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
/* The live refresh is the link being busy, and the pill says so -- but not
|
|
2355
|
+
* in the colours of an alarm. Something that lights up every three seconds
|
|
2356
|
+
* whether or not anything is wrong is a light nobody reads twice. */
|
|
2357
|
+
.pill.busy.quiet {
|
|
2358
|
+
border-color: var(--primary-deep);
|
|
2359
|
+
background: var(--surface);
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
.pill.busy.quiet .label {
|
|
2363
|
+
color: var(--text-dim);
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
.pill.busy.quiet .dot {
|
|
2367
|
+
background: var(--primary);
|
|
2368
|
+
animation: none;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
.pill.flashing,
|
|
2372
|
+
.pill.error {
|
|
2373
|
+
border-color: var(--danger-deep);
|
|
2374
|
+
background: color-mix(in srgb, var(--danger-deep) 30%, var(--surface));
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
.pill.error .dot {
|
|
2378
|
+
background: var(--danger);
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
.pill.error .label {
|
|
2382
|
+
color: var(--danger);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
/* The server itself is gone, so the pill is not describing a device at all.
|
|
2386
|
+
* Reddened, and that is all: it was struck through in one of the two, on
|
|
2387
|
+
* the reasoning that this is the page saying it cannot see rather than an
|
|
2388
|
+
* alarm about the device -- but a line through five small words is a line
|
|
2389
|
+
* through the one message on the page that has to be read at a glance. The
|
|
2390
|
+
* colour already says it. */
|
|
2391
|
+
.pill.gone {
|
|
2392
|
+
border-color: var(--danger-deep);
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
.pill.gone .dot {
|
|
2396
|
+
background: var(--danger);
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
.pill.gone .label {
|
|
2400
|
+
color: var(--danger);
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
@keyframes throb {
|
|
2404
|
+
0%,
|
|
2405
|
+
100% {
|
|
2406
|
+
opacity: 1;
|
|
2407
|
+
transform: scale(1);
|
|
2408
|
+
}
|
|
2409
|
+
50% {
|
|
2410
|
+
opacity: 0.45;
|
|
2411
|
+
transform: scale(0.82);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2416
|
+
/* Same specificity as the rule that starts it, and later in the file, so
|
|
2417
|
+
* the animation is cancelled without !important. */
|
|
2418
|
+
.pill.busy .dot,
|
|
2419
|
+
.pill.opening .dot,
|
|
2420
|
+
.pill.flashing .dot {
|
|
2421
|
+
animation: none;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
/* A popover hung off something in the header. The trigger is the anchor's
|
|
2426
|
+
* own child, so the menu opens under it rather than under the header. */
|
|
2427
|
+
.anchor {
|
|
2428
|
+
position: relative;
|
|
2429
|
+
display: flex;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
.menu {
|
|
2433
|
+
position: absolute;
|
|
2434
|
+
z-index: 30;
|
|
2435
|
+
top: calc(100% + 8px);
|
|
2436
|
+
right: 0;
|
|
2437
|
+
width: min(430px, calc(100vw - 2 * var(--gutter)));
|
|
2438
|
+
padding: 13px 15px;
|
|
2439
|
+
border: 1px solid var(--line-strong);
|
|
2440
|
+
border-radius: var(--radius);
|
|
2441
|
+
background: var(--surface-2);
|
|
2442
|
+
box-shadow: 0 12px 30px rgb(0 0 0 / 45%);
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
/* The tabs, on the header's own bottom rule and on the content column's
|
|
2446
|
+
* own left edge. Inside the header rather than under it: two hairlines a
|
|
2447
|
+
* row apart is a box drawn around the chrome, and the chrome is not a box. */
|
|
2448
|
+
nav.tabs {
|
|
2449
|
+
max-width: var(--page);
|
|
2450
|
+
margin: 0 auto;
|
|
2451
|
+
display: flex;
|
|
2452
|
+
gap: 2px;
|
|
2453
|
+
padding: 0 var(--gutter);
|
|
2454
|
+
overflow-x: auto;
|
|
2455
|
+
scrollbar-width: none;
|
|
2456
|
+
}
|
|
2457
|
+
nav.tabs::-webkit-scrollbar { display: none; }
|
|
2458
|
+
nav.tabs button {
|
|
2459
|
+
background: none;
|
|
2460
|
+
border: 0;
|
|
2461
|
+
border-bottom: 2px solid transparent;
|
|
2462
|
+
color: var(--text-dim);
|
|
2463
|
+
padding: 10px 16px;
|
|
2464
|
+
cursor: pointer;
|
|
2465
|
+
border-radius: 0;
|
|
2466
|
+
white-space: nowrap;
|
|
2467
|
+
/* The size the rest of the chrome is -- the pill, the badges, the offline
|
|
2468
|
+
* line. Written here rather than left to the body, because one page had
|
|
2469
|
+
* overridden it and the two tab strips were different sizes. */
|
|
2470
|
+
font-size: var(--fs-sm);
|
|
2471
|
+
font-weight: 550;
|
|
2472
|
+
}
|
|
2473
|
+
/* A tab you are not on has to look like a tab. Colour alone was the only
|
|
2474
|
+
* thing that moved on hover, which on a strip of nine words reads as text
|
|
2475
|
+
* that happens to brighten rather than as somewhere you can go -- so the
|
|
2476
|
+
* underline the selected tab wears appears in a quieter grey under the one
|
|
2477
|
+
* the pointer is on, and the label lifts off the canvas. */
|
|
2478
|
+
nav.tabs button:hover {
|
|
2479
|
+
color: var(--text);
|
|
2480
|
+
border-bottom-color: var(--line-strong);
|
|
2481
|
+
background: color-mix(in srgb, var(--surface) 65%, transparent);
|
|
2482
|
+
}
|
|
2483
|
+
nav.tabs button[aria-selected="true"] { color: var(--primary-bright); border-bottom-color: var(--primary); }
|
|
2484
|
+
nav.tabs button[aria-selected="true"]:hover { border-bottom-color: var(--primary); }
|
|
2485
|
+
|
|
2486
|
+
/* Nine tabs do not fit a phone, and a strip that is cut off dead at the
|
|
2487
|
+
* edge does not look scrollable. A fade at each end says there is more. */
|
|
2488
|
+
nav.tabs {
|
|
2489
|
+
mask-image: linear-gradient(90deg, transparent 0, #000 14px, #000 calc(100% - 14px), transparent 100%);
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
/* A tab that has been opened and is not the one being looked at. It keeps
|
|
2493
|
+
* everything -- what it read, what is half-typed in it, where it was
|
|
2494
|
+
* scrolled to -- and comes back instantly. */
|
|
2495
|
+
.pane[hidden] { display: none; }
|
|
2496
|
+
|
|
2497
|
+
main {
|
|
2498
|
+
padding: 24px var(--gutter) 40px;
|
|
2499
|
+
max-width: var(--page);
|
|
2500
|
+
width: 100%;
|
|
2501
|
+
margin: 0 auto;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
/* Narrower screens: the same page, not a cut-down one. The columns get
|
|
2505
|
+
* closer together and the label column gives up some of its width, and
|
|
2506
|
+
* nothing here shrinks the type -- a phone in a garage is the worst place
|
|
2507
|
+
* to read 11px labels. */
|
|
2508
|
+
@media (max-width: 900px) {
|
|
2509
|
+
:root {
|
|
2510
|
+
--grid-track: 300px;
|
|
2511
|
+
--label-col: 8rem;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
@media (max-width: 760px) {
|
|
2516
|
+
main {
|
|
2517
|
+
padding: 18px var(--gutter) 56px;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
/* The licence line: below all content, quiet, one line. The EU flag next
|
|
2522
|
+
* to it is the cue for what "EUPL" means even if the acronym says nothing
|
|
2523
|
+
* to you. The flag itself is an inline SVG in the official geometry; what
|
|
2524
|
+
* the stylesheet adds is the presentation. */
|
|
2525
|
+
.license {
|
|
2526
|
+
display: flex;
|
|
2527
|
+
align-items: center;
|
|
2528
|
+
justify-content: center;
|
|
2529
|
+
gap: 8px;
|
|
2530
|
+
padding: 0 var(--gutter) 28px;
|
|
2531
|
+
font-size: var(--fs-sm);
|
|
2532
|
+
color: var(--text-faint);
|
|
2533
|
+
}
|
|
2534
|
+
.license a { color: var(--text-faint); }
|
|
2535
|
+
.license a:hover { color: var(--primary-bright); }
|
|
2536
|
+
.license svg { flex: none; display: block; }
|
|
2537
|
+
|
|
2538
|
+
/* The flag is a dark blue rectangle, and this page paints its canvas
|
|
2539
|
+
* either near-black or near-white -- on both, a flat dark rectangle at
|
|
2540
|
+
* 30x20 sinks into the page. A hairline in the panel-line grey was not
|
|
2541
|
+
* enough to pick it out of the black, so the dark theme frames it in the
|
|
2542
|
+
* faint text grey instead: a light steel edge that reads as a sticker,
|
|
2543
|
+
* not a sunk rectangle. The drop shadow gives it a lift in both
|
|
2544
|
+
* themes. */
|
|
2545
|
+
.eu-flag {
|
|
2546
|
+
border: 1px solid var(--text-faint);
|
|
2547
|
+
border-radius: 3px;
|
|
2548
|
+
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.4));
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
/* Light theme: the flag's blue is dark enough to hold its own edge
|
|
2552
|
+
* against the light-grey page, so drop the border; the drop shadow
|
|
2553
|
+
* stays as the cue. */
|
|
2554
|
+
[data-theme="light"] .eu-flag { border: 0; }
|
|
2555
|
+
|
|
2556
|
+
/* --- what the pointer is over, on a plot ----------------------------------
|
|
2557
|
+
*
|
|
2558
|
+
* The plot itself is a program's own -- `.chart` is sized and coloured
|
|
2559
|
+
* beside the panel that draws it -- but the label that follows the
|
|
2560
|
+
* pointer across it is `js/chart.js`, and so is this.
|
|
2561
|
+
*/
|
|
2562
|
+
|
|
2563
|
+
/* The plot's own rectangle: whatever is drawn, and everything laid over
|
|
2564
|
+
* it. Everything below is positioned in percentages of this box, so this
|
|
2565
|
+
* is the box those percentages are of.
|
|
2566
|
+
*
|
|
2567
|
+
* It lived in one program's own stylesheet while the label that needs it
|
|
2568
|
+
* lived here, which held together for exactly as long as one program drew
|
|
2569
|
+
* plots: the first bar chart in the other one had a label that positioned
|
|
2570
|
+
* itself against the page. The rule belongs with the component that
|
|
2571
|
+
* depends on it. */
|
|
2572
|
+
.chart .plot { position: relative; }
|
|
2573
|
+
|
|
2574
|
+
/* What the pointer is over. Not interactive itself -- it follows the
|
|
2575
|
+
* pointer, so anything it caught would be caught from the plot underneath
|
|
2576
|
+
* it, and the reading would flicker as the label chased the cursor.
|
|
2577
|
+
*
|
|
2578
|
+
* Lifted above the page's own content, because the label leaves the plot:
|
|
2579
|
+
* it stands a few pixels clear of it, above a chart and below a band, and
|
|
2580
|
+
* whatever it lands on is a sibling further down the card. With no layer
|
|
2581
|
+
* of its own it was painted in source order against those siblings -- and
|
|
2582
|
+
* a range input carries `--slider-lift`, a drop-shadow, which is enough to
|
|
2583
|
+
* put it in front of anything drawn before it. A reading that a slider
|
|
2584
|
+
* further down the card is drawn on top of is a reading nobody can read.
|
|
2585
|
+
*
|
|
2586
|
+
* 5 is above everything in the page body and below all of the chrome: the
|
|
2587
|
+
* header is 20, a menu inside it 30, a dialog 60, the toasts 80. */
|
|
2588
|
+
.chart .hover { position: absolute; inset: 0; z-index: 5; pointer-events: none; }
|
|
2589
|
+
.chart .hover .rule {
|
|
2590
|
+
position: absolute;
|
|
2591
|
+
top: 1px;
|
|
2592
|
+
bottom: 1px;
|
|
2593
|
+
width: 1px;
|
|
2594
|
+
margin-left: -0.5px;
|
|
2595
|
+
background: var(--line-strong);
|
|
2596
|
+
}
|
|
2597
|
+
.chart .hover .spot {
|
|
2598
|
+
position: absolute;
|
|
2599
|
+
width: 7px;
|
|
2600
|
+
height: 7px;
|
|
2601
|
+
margin: -3.5px 0 0 -3.5px;
|
|
2602
|
+
border-radius: 50%;
|
|
2603
|
+
background: var(--live);
|
|
2604
|
+
box-shadow: 0 0 0 2px var(--surface-2);
|
|
2605
|
+
}
|
|
2606
|
+
/* The reading itself, above the plot rather than on it: over the line it
|
|
2607
|
+
* would cover the very shape it is explaining. */
|
|
2608
|
+
.chart .hover .tip {
|
|
2609
|
+
position: absolute;
|
|
2610
|
+
bottom: calc(100% + 6px);
|
|
2611
|
+
transform: translateX(-50%);
|
|
2612
|
+
display: grid;
|
|
2613
|
+
gap: 1px;
|
|
2614
|
+
padding: 5px 9px;
|
|
2615
|
+
border: 1px solid var(--line-strong);
|
|
2616
|
+
border-radius: var(--radius-sm);
|
|
2617
|
+
background: var(--surface);
|
|
2618
|
+
box-shadow: 0 6px 16px rgb(0 0 0 / 35%);
|
|
2619
|
+
white-space: nowrap;
|
|
2620
|
+
font-size: var(--fs-xs);
|
|
2621
|
+
font-variant-numeric: tabular-nums;
|
|
2622
|
+
line-height: 1.35;
|
|
2623
|
+
}
|
|
2624
|
+
/* Under the plot instead of over it, for something that is a strip rather
|
|
2625
|
+
* than a rectangle: see `below` in js/chart.js. */
|
|
2626
|
+
.chart .hover .tip.below {
|
|
2627
|
+
top: calc(100% + 6px);
|
|
2628
|
+
bottom: auto;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
.chart .hover .tip .when { color: var(--text-faint); }
|
|
2632
|
+
.chart .hover .tip .what { color: var(--text); font-weight: 560; }
|
|
2633
|
+
|
|
2634
|
+
/* --- a reading over time --------------------------------------------------
|
|
2635
|
+
*
|
|
2636
|
+
* `Series` in js/chart.js. The line, the fill under it, the marks where
|
|
2637
|
+
* something was written, and the row of ends under the lot. These lived in
|
|
2638
|
+
* one program's own stylesheet beside the component, and moved here with it
|
|
2639
|
+
* when the other program's copy of the same chart turned out to be drawing
|
|
2640
|
+
* in colours that no longer existed.
|
|
2641
|
+
*
|
|
2642
|
+
* `--live` is what both pages mean by something happening right now, and
|
|
2643
|
+
* the line is the live number's own history rather than a second
|
|
2644
|
+
* measurement -- so it is drawn in the same colour as the number above it.
|
|
2645
|
+
*/
|
|
2646
|
+
.chart {
|
|
2647
|
+
display: block;
|
|
2648
|
+
width: 100%;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
.chart svg {
|
|
2652
|
+
display: block;
|
|
2653
|
+
width: 100%;
|
|
2654
|
+
height: 108px;
|
|
2655
|
+
border: 1px solid var(--line);
|
|
2656
|
+
border-radius: var(--radius-sm);
|
|
2657
|
+
background: var(--surface-2);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
.chart .line {
|
|
2661
|
+
fill: none;
|
|
2662
|
+
stroke: var(--live);
|
|
2663
|
+
stroke-width: 1.75;
|
|
2664
|
+
stroke-linejoin: round;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
/* A second reading on the same plot, on a scale of its own. `--chart-alt`
|
|
2668
|
+
* so a program can part it from the page's own accent: it has to be told
|
|
2669
|
+
* apart from the first line at a glance and from the write marks as well,
|
|
2670
|
+
* and which colour manages that depends on what `--live` already is. It is
|
|
2671
|
+
* drawn thinner than the first, because it is the answer to a question
|
|
2672
|
+
* about the first rather than a second subject. */
|
|
2673
|
+
.chart .line.alt {
|
|
2674
|
+
stroke: var(--chart-alt, var(--primary-bright));
|
|
2675
|
+
stroke-width: 1.4;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
.chart .area {
|
|
2679
|
+
fill: color-mix(in srgb, var(--live) 16%, transparent);
|
|
2680
|
+
stroke: none;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
/* Zero, where the reading goes both ways. A pack that charges and
|
|
2684
|
+
* discharges is drawn either side of this; one that only ever draws power
|
|
2685
|
+
* has its baseline along the bottom and no rule at all, because a rule
|
|
2686
|
+
* along the frame is a second frame. */
|
|
2687
|
+
.chart .axis {
|
|
2688
|
+
stroke: var(--line);
|
|
2689
|
+
fill: none;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
/* Where a setting was written. Faint, and behind nothing: the question it
|
|
2693
|
+
* answers is "did that do anything", so it has to be next to the line
|
|
2694
|
+
* without competing with it. */
|
|
2695
|
+
.chart .mark {
|
|
2696
|
+
stroke: var(--primary);
|
|
2697
|
+
stroke-width: 1;
|
|
2698
|
+
stroke-dasharray: 3 3;
|
|
2699
|
+
opacity: 0.75;
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
/* Under the plot: how far back it goes, and what is on it.
|
|
2703
|
+
*
|
|
2704
|
+
* Two shapes, because there are two things to lay out. A chart of one
|
|
2705
|
+
* reading has two facts -- the window and the scale -- and they go to the
|
|
2706
|
+
* two ends, which is where a reader looks for the ends of an axis. A chart
|
|
2707
|
+
* of two has a key per line as well, and pushed apart those read as three
|
|
2708
|
+
* unrelated remarks across a line that is mostly gap; `keyed` packs them
|
|
2709
|
+
* into a list instead, wrapping onto a second line on a narrow card.
|
|
2710
|
+
*/
|
|
2711
|
+
.chart .ends {
|
|
2712
|
+
display: flex;
|
|
2713
|
+
flex-wrap: wrap;
|
|
2714
|
+
justify-content: space-between;
|
|
2715
|
+
gap: 2px 12px;
|
|
2716
|
+
margin-top: 5px;
|
|
2717
|
+
color: var(--text-faint);
|
|
2718
|
+
font-size: var(--fs-xs);
|
|
2719
|
+
font-variant-numeric: tabular-nums;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
.chart .ends.keyed {
|
|
2723
|
+
justify-content: flex-start;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
.chart .ends .key {
|
|
2727
|
+
display: inline-flex;
|
|
2728
|
+
align-items: center;
|
|
2729
|
+
gap: 5px;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
/* The key's own sample of the mark it is naming, drawn the same way: a
|
|
2733
|
+
* dashed upright for a write, a length of the line itself for a reading. */
|
|
2734
|
+
.chart .ends .key i.tick {
|
|
2735
|
+
display: inline-block;
|
|
2736
|
+
width: 0;
|
|
2737
|
+
height: 10px;
|
|
2738
|
+
border-left: 1px dashed var(--primary);
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
.chart .ends .key i.swatch {
|
|
2742
|
+
display: inline-block;
|
|
2743
|
+
width: 14px;
|
|
2744
|
+
height: 0;
|
|
2745
|
+
border-top: 2px solid var(--live);
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
.chart .ends .key i.swatch.alt {
|
|
2749
|
+
border-top-color: var(--chart-alt, var(--primary-bright));
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
/* A chart with not enough readings to be a chart yet. Dashed, and the size
|
|
2753
|
+
* of the plot it will become, so the card does not change shape when the
|
|
2754
|
+
* second sample lands. */
|
|
2755
|
+
.chart.empty-chart {
|
|
2756
|
+
color: var(--text-faint);
|
|
2757
|
+
font-size: var(--fs-sm);
|
|
2758
|
+
border: 1px dashed var(--line);
|
|
2759
|
+
border-radius: var(--radius-sm);
|
|
2760
|
+
padding: 24px 14px;
|
|
2761
|
+
text-align: center;
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
/* --- a reading against the limits it has to stay inside -------------------
|
|
2765
|
+
*
|
|
2766
|
+
* `js/band.js`. One picture with the reading and every setpoint that
|
|
2767
|
+
* bears on it, because none of them says anything on its own: 42 °C is
|
|
2768
|
+
* fine under a 60 °C protection and a fault under a 40 °C one.
|
|
2769
|
+
*
|
|
2770
|
+
* It is laid out over `.chart .plot`, like the plots are, so that the
|
|
2771
|
+
* label that follows the pointer is the same label -- the same box, in the
|
|
2772
|
+
* same place, appearing at the same moment -- as the one on a chart. Two
|
|
2773
|
+
* different tooltips on one page is two things to learn.
|
|
2774
|
+
*/
|
|
2775
|
+
.band { margin: 12px 0 2px; }
|
|
2776
|
+
|
|
2777
|
+
/* What this band is about, where a card draws more than one of them. A
|
|
2778
|
+
* temperature card with the cells' band over the MOSFETs' is two strips
|
|
2779
|
+
* that look alike and mean different things, and a scale that reads -2 to
|
|
2780
|
+
* 32 over one that reads 27 to 52 is not the difference between them. */
|
|
2781
|
+
.band .cap {
|
|
2782
|
+
margin-bottom: 5px;
|
|
2783
|
+
color: var(--text-dim);
|
|
2784
|
+
font-size: var(--fs-xs);
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
/* --- every number on the band, written above it --------------------------
|
|
2788
|
+
*
|
|
2789
|
+
* The reading and each setpoint, printed where it sits. A hover label is
|
|
2790
|
+
* an answer to a question somebody already knew to ask; these are what
|
|
2791
|
+
* make the picture answer "so what is it, and what are the limits" without
|
|
2792
|
+
* being asked, on a phone, and in a screenshot pasted into a message.
|
|
2793
|
+
*
|
|
2794
|
+
* Laid out in `js/band.js`, which knows how wide the track is: the rows,
|
|
2795
|
+
* and how far along each label goes, come in as inline positions.
|
|
2796
|
+
*/
|
|
2797
|
+
.band .marks {
|
|
2798
|
+
position: relative;
|
|
2799
|
+
margin-bottom: 4px;
|
|
2800
|
+
font-size: var(--fs-xs);
|
|
2801
|
+
font-variant-numeric: tabular-nums;
|
|
2802
|
+
line-height: 1;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
.band .tag {
|
|
2806
|
+
position: absolute;
|
|
2807
|
+
display: inline-flex;
|
|
2808
|
+
align-items: center;
|
|
2809
|
+
gap: 4px;
|
|
2810
|
+
height: 14px;
|
|
2811
|
+
padding: 0 3px;
|
|
2812
|
+
transform: translateX(-50%);
|
|
2813
|
+
border: 1px solid transparent;
|
|
2814
|
+
border-radius: 4px;
|
|
2815
|
+
/* The card's own colour rather than nothing, so that the hairline from a
|
|
2816
|
+
* number higher up passes behind the numbers below it instead of through
|
|
2817
|
+
* a digit. */
|
|
2818
|
+
background: var(--surface);
|
|
2819
|
+
color: var(--text-dim);
|
|
2820
|
+
white-space: nowrap;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
/* The reading is the one number on the strip somebody came to read; the
|
|
2824
|
+
* setpoints are what it is being read against. */
|
|
2825
|
+
.band .tag.read {
|
|
2826
|
+
color: var(--text);
|
|
2827
|
+
font-weight: 600;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
/* Held but not written, in the colour the row beside it goes -- see
|
|
2831
|
+
* `input.pending`. */
|
|
2832
|
+
.band .tag.pending { color: var(--live); }
|
|
2833
|
+
|
|
2834
|
+
/* Two things on one value: the release still sitting on the protection it
|
|
2835
|
+
* releases, or the pack sitting exactly on a limit. One number, a mark
|
|
2836
|
+
* for each of them, and a box around the lot so that it reads as several
|
|
2837
|
+
* things agreeing rather than as one. */
|
|
2838
|
+
.band .tag.many {
|
|
2839
|
+
border-color: var(--line-strong);
|
|
2840
|
+
background: var(--surface-2);
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
/* A number lifted off the track because the one beside it was already
|
|
2844
|
+
* there, joined back to its own place on the scale. */
|
|
2845
|
+
.band .stem {
|
|
2846
|
+
position: absolute;
|
|
2847
|
+
bottom: 0;
|
|
2848
|
+
width: 1px;
|
|
2849
|
+
margin-left: -0.5px;
|
|
2850
|
+
background: var(--line);
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
/* The same four marks as on the track, small enough to sit in a label:
|
|
2854
|
+
* a protection, the release beside it, a setpoint that protects against
|
|
2855
|
+
* nothing, and the reading.
|
|
2856
|
+
*
|
|
2857
|
+
* Square, where the setpoint on the track is round. They used to be the
|
|
2858
|
+
* same disc in both places, which said the two were the same thing when
|
|
2859
|
+
* only one of them is: the dot on the track is a handle and can be
|
|
2860
|
+
* dragged, the dot in the label is a key to what the number above it
|
|
2861
|
+
* belongs to and does nothing at all. A reader who tried to drag one
|
|
2862
|
+
* found that out by trying. Round is now the whole vocabulary for "take
|
|
2863
|
+
* hold of this"; the label's marks keep the colour and the hollowness --
|
|
2864
|
+
* which is what says *which* setpoint -- and give up the shape.
|
|
2865
|
+
*
|
|
2866
|
+
* The reading's mark is left alone: it is already a bar rather than a dot,
|
|
2867
|
+
* because the reading on the track is a needle rather than a grip. */
|
|
2868
|
+
.band .pip {
|
|
2869
|
+
flex: none;
|
|
2870
|
+
width: 7px;
|
|
2871
|
+
height: 7px;
|
|
2872
|
+
border-radius: 1.5px;
|
|
2873
|
+
background: var(--text-dim);
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
.band .pip.low,
|
|
2877
|
+
.band .pip.high { background: var(--danger); }
|
|
2878
|
+
|
|
2879
|
+
.band .pip.soft {
|
|
2880
|
+
background: var(--surface);
|
|
2881
|
+
box-shadow: inset 0 0 0 2px var(--text-dim);
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
.band .pip.low.soft,
|
|
2885
|
+
.band .pip.high.soft { box-shadow: inset 0 0 0 2px var(--danger); }
|
|
2886
|
+
|
|
2887
|
+
.band .pip.read {
|
|
2888
|
+
width: 3px;
|
|
2889
|
+
height: 9px;
|
|
2890
|
+
border-radius: 2px;
|
|
2891
|
+
background: var(--text);
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
/* Room above and below the track for the setpoints, which stand taller
|
|
2895
|
+
* than it: the plot is the box the hover label measures itself against, so
|
|
2896
|
+
* anything drawn outside it would be a hit area the label disagrees with. */
|
|
2897
|
+
.band .plot {
|
|
2898
|
+
height: 22px;
|
|
2899
|
+
display: flex;
|
|
2900
|
+
align-items: center;
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
/* Horizontal drags belong to the setpoints; vertical ones are the page
|
|
2904
|
+
* scrolling past, which a band across a phone must not swallow. */
|
|
2905
|
+
.band .plot.settable {
|
|
2906
|
+
touch-action: pan-y;
|
|
2907
|
+
cursor: pointer;
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
.band .track {
|
|
2911
|
+
position: relative;
|
|
2912
|
+
width: 100%;
|
|
2913
|
+
height: 8px;
|
|
2914
|
+
border: 1px solid var(--line);
|
|
2915
|
+
border-radius: 999px;
|
|
2916
|
+
background: var(--surface-3);
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
/* Where nothing trips: from the highest floor to the lowest ceiling. */
|
|
2920
|
+
.band .safe {
|
|
2921
|
+
position: absolute;
|
|
2922
|
+
top: 0;
|
|
2923
|
+
bottom: 0;
|
|
2924
|
+
border-radius: 999px;
|
|
2925
|
+
background: color-mix(in srgb, var(--ok) 45%, var(--surface-3));
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
/* A reading that is a range rather than a point -- sixteen cells are not
|
|
2929
|
+
* one voltage -- drawn as the stretch the readings actually cover. On a
|
|
2930
|
+
* balanced pack that stretch is a few pixels wide, which is the answer. */
|
|
2931
|
+
.band .spread {
|
|
2932
|
+
position: absolute;
|
|
2933
|
+
top: -2px;
|
|
2934
|
+
bottom: -2px;
|
|
2935
|
+
min-width: 2px;
|
|
2936
|
+
border-radius: 999px;
|
|
2937
|
+
background: var(--text);
|
|
2938
|
+
opacity: 0.85;
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
.band .needle {
|
|
2942
|
+
position: absolute;
|
|
2943
|
+
top: -4px;
|
|
2944
|
+
width: 3px;
|
|
2945
|
+
height: 14px;
|
|
2946
|
+
margin-left: -1.5px;
|
|
2947
|
+
border-radius: 2px;
|
|
2948
|
+
background: var(--text);
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
.band .needle.bad { background: var(--danger); }
|
|
2952
|
+
|
|
2953
|
+
/* A setpoint. Round, so that it reads as something to take hold of rather
|
|
2954
|
+
* than as another mark on the scale. A protection is drawn in the colour
|
|
2955
|
+
* of a fault whichever end of the band it is on -- which side it is is
|
|
2956
|
+
* already said by which side of the calm stretch it sits on -- and a
|
|
2957
|
+
* setpoint that is not a protection at all is grey. */
|
|
2958
|
+
.band .grip {
|
|
2959
|
+
position: absolute;
|
|
2960
|
+
top: 50%;
|
|
2961
|
+
width: 11px;
|
|
2962
|
+
height: 11px;
|
|
2963
|
+
margin: -5.5px 0 0 -5.5px;
|
|
2964
|
+
padding: 0;
|
|
2965
|
+
border: 2px solid var(--surface);
|
|
2966
|
+
border-radius: 50%;
|
|
2967
|
+
background: var(--text-dim);
|
|
2968
|
+
box-shadow: 0 0 0 1px var(--line-strong);
|
|
2969
|
+
cursor: grab;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
.band .grip.low,
|
|
2973
|
+
.band .grip.high { background: var(--danger); }
|
|
2974
|
+
.band .grip.mark { background: var(--text-dim); }
|
|
2975
|
+
|
|
2976
|
+
/* The release point beside a protection: the temperature the device lets
|
|
2977
|
+
* the pack work again at. Smaller and hollow, because it is not a limit
|
|
2978
|
+
* -- the calm band reaches past it to the protection itself -- and a
|
|
2979
|
+
* reader who cannot tell the two apart will set the wrong one. */
|
|
2980
|
+
.band .grip.soft {
|
|
2981
|
+
width: 9px;
|
|
2982
|
+
height: 9px;
|
|
2983
|
+
margin: -4.5px 0 0 -4.5px;
|
|
2984
|
+
background: var(--surface);
|
|
2985
|
+
box-shadow: 0 0 0 2px var(--text-dim);
|
|
2986
|
+
border-width: 2px;
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
.band .grip.low.soft,
|
|
2990
|
+
.band .grip.high.soft { box-shadow: 0 0 0 2px var(--danger); }
|
|
2991
|
+
|
|
2992
|
+
/* A value the card is holding but has not written yet, in the same colour
|
|
2993
|
+
* the number in the row beside it goes -- see `input.pending`. A picture
|
|
2994
|
+
* and a table editing one draft have to agree about which of them is
|
|
2995
|
+
* unsent. */
|
|
2996
|
+
.band .grip.pending { box-shadow: 0 0 0 2px var(--live); }
|
|
2997
|
+
|
|
2998
|
+
.band .grip.fixed { cursor: default; }
|
|
2999
|
+
|
|
3000
|
+
/* A grip that has the keyboard is drawn as having it, however it got it.
|
|
3001
|
+
*
|
|
3002
|
+
* `:focus-visible` is the usual rule and is the wrong one here: it is false
|
|
3003
|
+
* for a button focused by a pointer, which is exactly the grip somebody has
|
|
3004
|
+
* just dragged and is about to nudge with the arrow keys. A ring that
|
|
3005
|
+
* appeared only after a Tab would mean the one affordance that says "this
|
|
3006
|
+
* still answers to the keyboard" is hidden from everybody who used the
|
|
3007
|
+
* mouse. So the ring is on `:focus`, and it is the page's own ring from
|
|
3008
|
+
* the top of this file, drawn here because the rule up there is
|
|
3009
|
+
* `:focus-visible`. */
|
|
3010
|
+
.band button.grip:hover,
|
|
3011
|
+
.band button.grip:focus { transform: scale(1.25); }
|
|
3012
|
+
|
|
3013
|
+
.band button.grip:focus {
|
|
3014
|
+
outline: 2px solid var(--primary);
|
|
3015
|
+
outline-offset: 2px;
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
.band button.grip:active { cursor: grabbing; }
|
|
3019
|
+
|
|
3020
|
+
.band .scale {
|
|
3021
|
+
display: flex;
|
|
3022
|
+
justify-content: space-between;
|
|
3023
|
+
gap: 8px;
|
|
3024
|
+
margin-top: 5px;
|
|
3025
|
+
color: var(--text-faint);
|
|
3026
|
+
font-size: var(--fs-xs);
|
|
3027
|
+
font-variant-numeric: tabular-nums;
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
.band .scale .mid {
|
|
3031
|
+
flex: 1;
|
|
3032
|
+
text-align: center;
|
|
3033
|
+
font-variant-numeric: normal;
|
|
3034
|
+
}
|