astro-dev-edit 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/package.json +52 -0
  4. package/src/client/admin-bar.ts +622 -0
  5. package/src/client/api.ts +370 -0
  6. package/src/client/classify-cache.ts +61 -0
  7. package/src/client/css-inspect.ts +345 -0
  8. package/src/client/editors/asset-picker.ts +155 -0
  9. package/src/client/editors/body-editor.ts +419 -0
  10. package/src/client/editors/collections-panel.ts +1532 -0
  11. package/src/client/editors/copy-panel.ts +73 -0
  12. package/src/client/editors/drawer.ts +95 -0
  13. package/src/client/editors/entry.ts +433 -0
  14. package/src/client/editors/expression.ts +77 -0
  15. package/src/client/editors/fields.ts +309 -0
  16. package/src/client/editors/image.ts +268 -0
  17. package/src/client/editors/markup-insert.ts +73 -0
  18. package/src/client/editors/markup.ts +125 -0
  19. package/src/client/editors/media-grid.ts +326 -0
  20. package/src/client/editors/media-modal.ts +588 -0
  21. package/src/client/editors/notice.ts +160 -0
  22. package/src/client/editors/peek.ts +135 -0
  23. package/src/client/editors/settings-panel.ts +457 -0
  24. package/src/client/editors/source-popup.ts +166 -0
  25. package/src/client/editors/text.ts +105 -0
  26. package/src/client/editors/unsplash-pane.ts +317 -0
  27. package/src/client/element-context.ts +308 -0
  28. package/src/client/features.ts +81 -0
  29. package/src/client/focus.ts +166 -0
  30. package/src/client/group.ts +186 -0
  31. package/src/client/highlight.ts +146 -0
  32. package/src/client/hover.ts +485 -0
  33. package/src/client/icons.ts +160 -0
  34. package/src/client/markdown.ts +319 -0
  35. package/src/client/overlay.ts +466 -0
  36. package/src/client/page-source.ts +143 -0
  37. package/src/client/router.ts +198 -0
  38. package/src/client/shadow.ts +111 -0
  39. package/src/client/source-map.ts +150 -0
  40. package/src/client/state.ts +153 -0
  41. package/src/client/styles.ts +3485 -0
  42. package/src/client/tree-model.ts +45 -0
  43. package/src/client/tree.ts +366 -0
  44. package/src/client/ui.ts +987 -0
  45. package/src/client/unsplash-search.ts +250 -0
  46. package/src/index.ts +299 -0
  47. package/src/patcher/astro.ts +792 -0
  48. package/src/patcher/content-config.ts +1035 -0
  49. package/src/patcher/dotenv.ts +121 -0
  50. package/src/patcher/expression-trace.ts +326 -0
  51. package/src/patcher/frontmatter.ts +249 -0
  52. package/src/patcher/registry.ts +11 -0
  53. package/src/patcher/types.ts +32 -0
  54. package/src/server/annotate.ts +173 -0
  55. package/src/server/assets.ts +167 -0
  56. package/src/server/collection-entries.ts +91 -0
  57. package/src/server/content-config.ts +210 -0
  58. package/src/server/editor.ts +15 -0
  59. package/src/server/entry-detect.ts +110 -0
  60. package/src/server/entry-resolve-routes.ts +218 -0
  61. package/src/server/entry-routes.ts +304 -0
  62. package/src/server/inspect-locate.ts +81 -0
  63. package/src/server/inspect-routes.ts +94 -0
  64. package/src/server/middleware.ts +480 -0
  65. package/src/server/options.ts +778 -0
  66. package/src/server/page-source-routes.ts +71 -0
  67. package/src/server/paths.ts +219 -0
  68. package/src/server/private-files.ts +116 -0
  69. package/src/server/route-manifest.ts +200 -0
  70. package/src/server/router.ts +94 -0
  71. package/src/server/schema-introspect.ts +233 -0
  72. package/src/server/schema-routes.ts +808 -0
  73. package/src/server/settings-routes.ts +246 -0
  74. package/src/server/settings.ts +382 -0
  75. package/src/server/text-writes.ts +105 -0
  76. package/src/server/unsplash-routes.ts +515 -0
  77. package/src/server/zod-adapt.ts +239 -0
  78. package/src/shared/asset-path.ts +132 -0
  79. package/src/shared/protocol.ts +935 -0
  80. package/src/shared/slug.ts +17 -0
  81. package/src/shared/unsplash.ts +51 -0
@@ -0,0 +1,3485 @@
1
+ /**
2
+ * The overlay's one stylesheet, adopted by the shadow root (shadow.ts).
3
+ *
4
+ * Two jobs:
5
+ *
6
+ * 1. **Close the inheritance leak.** Selectors do not cross a shadow boundary,
7
+ * but inheritance does — a host page's `body { font-family }` and `color`
8
+ * reach every node in here unless stopped. `:host { all: initial }` stops
9
+ * them, once, and the declarations after it establish what the overlay
10
+ * inherits instead.
11
+ *
12
+ * 2. **Publish the tokens as custom properties.** Custom properties *do* cross
13
+ * the boundary, which makes them the overlay's theming API: a user writes
14
+ * `astro-dev-edit { --atx-card: #101014 }` in their own CSS and it lands.
15
+ * See docs/STYLING.md.
16
+ *
17
+ * **The variable block is generated from `ui.ts`'s tokens, never hand-written.**
18
+ * `tests/contrast.test.ts` imports `COLOR` and is the only thing standing
19
+ * between a token and an illegible panel; a hand-maintained CSS copy could
20
+ * drift and the test would still pass while the UI was wrong. Naming rule:
21
+ * camelCase to kebab, prefixed — `mutedFg` → `--atx-muted-fg`.
22
+ *
23
+ * `overlayCss()` is a **function**, not a constant, because ui.ts → shadow.ts →
24
+ * styles.ts → ui.ts is an import cycle: reading `COLOR` at module-evaluation
25
+ * time would hit the temporal dead zone. Reading it at first mount does not.
26
+ */
27
+
28
+ import { BAR_CHIP, BAR_CHIP_HOVER, CHECKER, COLOR, FONT, RADIUS, Z, hexToRgba, lift } from './ui.ts';
29
+
30
+ const kebab = (k: string): string => k.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
31
+
32
+ function vars(prefix: string, tokens: Record<string, string>): string {
33
+ return Object.entries(tokens)
34
+ .map(([k, v]) => ` --atx-${prefix}${kebab(k)}: ${v};`)
35
+ .join('\n');
36
+ }
37
+
38
+ /** The admin bar's height. Mirrors BAR_H in admin-bar.ts, which reports the
39
+ * same number to setChromeInset so docked surfaces keep clear of it. */
40
+ const BAR_H = 36;
41
+
42
+ /** The checkbox tick, as a mask rather than a glyph: the shape comes from here
43
+ * and the colour from whatever `background` the rule sets, so the tick tracks
44
+ * `primaryFg` without a second copy of the path living in CSS. Lucide's
45
+ * `check`, at the stroke weight a 12px box needs to stay crisp. */
46
+ const CHECK_MASK =
47
+ "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E\") center / contain no-repeat";
48
+
49
+ /** Lucide's `chevron-down` as a `background-image`, in a colour baked in at
50
+ * stylesheet-build time. A select cannot carry a pseudo-element and a mask
51
+ * would clip the control itself, so the arrow has to be a background — which
52
+ * means it cannot read a custom property, hence the argument. */
53
+ function chevronUrl(color: string): string {
54
+ const svg =
55
+ `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' ` +
56
+ `stroke='${color}' stroke-width='2' stroke-linecap='round' ` +
57
+ `stroke-linejoin='round'><path d='m6 9.5 6 6 6-6'/></svg>`;
58
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
59
+ }
60
+
61
+ export function overlayCss(): string {
62
+ return `
63
+ :host {
64
+ /* The guard. Everything the host page could inherit into the overlay stops
65
+ here; the declarations below are what the overlay inherits instead. */
66
+ all: initial;
67
+
68
+ /* No box at all — the host cannot shift the page's layout, and every
69
+ absolutely-positioned child still resolves against the initial containing
70
+ block, exactly as it did as a child of <body>. */
71
+ display: contents;
72
+
73
+ ${vars('', COLOR)}
74
+ ${vars('radius-', RADIUS)}
75
+ ${vars('font-', FONT)}
76
+
77
+ font: 400 14px/1.45 var(--atx-font-ui);
78
+ color: var(--atx-foreground);
79
+ cursor: auto;
80
+ /* Light type on a near-black ground blooms under subpixel rendering, which
81
+ is what reads as fringed or crunchy. Grayscale antialiasing on both
82
+ engines, plus kerning and the discretionary pairs, is the whole of it;
83
+ everything inside the root inherits all four. */
84
+ -webkit-font-smoothing: antialiased;
85
+ -moz-osx-font-smoothing: grayscale;
86
+ text-rendering: optimizeLegibility;
87
+ font-kerning: normal;
88
+ text-align: left;
89
+ direction: ltr;
90
+ }
91
+
92
+ /* Form controls carry their own UA font reset, so the smoothing and kerning
93
+ set on :host stop at their boundary and they render a shade crunchier than
94
+ the prose beside them. Restating the four here is the only way across. */
95
+ input,
96
+ textarea,
97
+ select,
98
+ button {
99
+ -webkit-font-smoothing: inherit;
100
+ -moz-osx-font-smoothing: inherit;
101
+ text-rendering: inherit;
102
+ font-kerning: inherit;
103
+ }
104
+
105
+ /* ── Shells ────────────────────────────────────────────────────────────────
106
+ The panel, drawer, backdrop and tab strip built by ui.ts. What stays inline
107
+ at those call sites is only what cannot be known here: the computed stacking
108
+ layer (Z_MODAL + n), a caller's width or height override, and the sized-panel
109
+ branch, which is a [data-sized] flag rather than an inline display so the
110
+ layout lives in one place. */
111
+
112
+ .atx-panel {
113
+ position: fixed;
114
+ left: 50%;
115
+ top: 50%;
116
+ transform: translate(-50%, -50%);
117
+ width: min(420px, 92vw);
118
+ box-sizing: border-box;
119
+ overflow: hidden;
120
+ border: 1px solid transparent;
121
+ border-radius: var(--atx-radius-xl);
122
+ background: var(--atx-card);
123
+ color: var(--atx-foreground);
124
+ /* A ring separates the surfaces; the shadow only lifts the panel off the
125
+ page behind it. Two layers rather than one border, because this floats
126
+ over content the overlay does not control and a hairline alone would
127
+ disappear against a light page. */
128
+ box-shadow: 0 0 0 1px var(--atx-border), 0 8px 28px rgba(0, 0, 0, 0.4);
129
+ font: 400 14px var(--atx-font-ui);
130
+ /* Edit mode sets a crosshair cursor on the whole page; our UI is not a
131
+ click-to-edit surface, so restore normal per-element cursors. */
132
+ cursor: auto;
133
+ }
134
+
135
+ /* A sized panel lays its title/body/foot out as a column so the body is the
136
+ only part that grows; the default auto-height panel is unaffected. */
137
+ .atx-panel[data-sized] {
138
+ display: flex;
139
+ flex-direction: column;
140
+ }
141
+
142
+ .atx-panel-title {
143
+ display: flex;
144
+ align-items: center;
145
+ gap: 8px;
146
+ padding: 16px 20px;
147
+ border-bottom: 1px solid var(--atx-border);
148
+ font: 500 14px var(--atx-font-ui);
149
+ }
150
+
151
+ .atx-panel-heading {
152
+ flex: 1;
153
+ min-width: 0;
154
+ overflow: hidden;
155
+ text-overflow: ellipsis;
156
+ white-space: nowrap;
157
+ }
158
+
159
+ .atx-panel-body {
160
+ padding: 20px;
161
+ }
162
+
163
+ /* A body whose content brings its own edges — the source peek's code pane
164
+ runs to the panel's sides. */
165
+ .atx-panel-body[data-flush] {
166
+ padding: 0;
167
+ }
168
+
169
+ /* In a sized panel the body absorbs the leftover height and scrolls;
170
+ 'min-height: 0' is what lets a flex child actually shrink to do that. */
171
+ .atx-panel[data-sized] .atx-panel-body {
172
+ flex: 1 1 auto;
173
+ min-height: 0;
174
+ overflow-y: auto;
175
+ }
176
+
177
+ /* Same band as the drawer's: a rule and a ground one step down from the panel,
178
+ so the row that completes the modal is plainly not part of its content. */
179
+ .atx-panel-foot {
180
+ display: flex;
181
+ align-items: center;
182
+ justify-content: flex-end;
183
+ gap: 8px;
184
+ padding: 16px 20px;
185
+ border-top: 1px solid var(--atx-border);
186
+ border-radius: 0 0 var(--atx-radius-xl) var(--atx-radius-xl);
187
+ background: var(--atx-background);
188
+ }
189
+
190
+ .atx-panel[data-sized] .atx-panel-foot {
191
+ flex: 0 0 auto;
192
+ }
193
+
194
+ .atx-drawer {
195
+ position: fixed;
196
+ right: 0;
197
+ top: 0;
198
+ display: flex;
199
+ flex-direction: column;
200
+ /* Half the screen, but never narrower than the classic 440px drawer and
201
+ never wider than the viewport allows on small screens. */
202
+ height: 100vh;
203
+ width: min(max(440px, 50vw), 94vw);
204
+ box-sizing: border-box;
205
+ border-left: 1px solid var(--atx-border);
206
+ /* The scrolling body is the canvas, one step darker than the cards on it --
207
+ which is the whole reason a card reads as a bounded thing here. The
208
+ title bar and the footer band paint themselves back up to the card
209
+ surface, so the drawer reads as chrome around content. */
210
+ background: var(--atx-background);
211
+ color: var(--atx-foreground);
212
+ box-shadow: -4px 0 16px rgba(0, 0, 0, 0.32);
213
+ font: 400 14px var(--atx-font-ui);
214
+ cursor: auto;
215
+ }
216
+
217
+ /* The drawer's title bar is a card header: a grid so the name and the line
218
+ under it both stop short of the corner action instead of running under it,
219
+ and so the action stays pinned to the top-right however tall the text
220
+ column grows. Two columns only when there is something to put in the
221
+ second -- [data-action], set by group.ts::cardHead. */
222
+ .atx-drawer-title {
223
+ display: grid;
224
+ grid-template-columns: 1fr;
225
+ align-items: start;
226
+ gap: 2px 12px;
227
+ flex: 0 0 auto;
228
+ padding: 16px 20px;
229
+ border-bottom: 1px solid var(--atx-border);
230
+ background: var(--atx-card);
231
+ }
232
+
233
+ /* The second column appears only once a caller has actually put something in
234
+ the action slot. :has() rather than a flag the caller has to remember to
235
+ set: the slot is filled after the drawer is built, and a header that
236
+ reserved the column unconditionally would pull the title short of an edge
237
+ with nothing at it. */
238
+ .atx-drawer-title:has([data-actions] > *) {
239
+ grid-template-columns: 1fr auto;
240
+ }
241
+
242
+ /* The name and anything qualifying it. min-width: 0 so the title inside can
243
+ still ellipsize -- a grid item defaults to min-content, which would let the
244
+ name push the header wider instead of shortening. */
245
+ .atx-drawer-name {
246
+ display: flex;
247
+ align-items: center;
248
+ gap: 8px;
249
+ min-width: 0;
250
+ }
251
+
252
+ /* 16px/500 is the only type at that size in a drawer, which is what makes it
253
+ read as the name of the thing rather than as the first of the labels. */
254
+ .atx-drawer-title-text {
255
+ min-width: 0;
256
+ overflow: hidden;
257
+ text-overflow: ellipsis;
258
+ white-space: nowrap;
259
+ color: var(--atx-foreground);
260
+ font: 500 16px/1.4 var(--atx-font-ui);
261
+ }
262
+
263
+ /* The line under the name: which file, which collection, what the caveat is.
264
+ Muted ink at body size -- separated from the title by weight and colour,
265
+ not by shrinking it into small print. */
266
+ .atx-drawer-subtitle {
267
+ min-width: 0;
268
+ overflow: hidden;
269
+ text-overflow: ellipsis;
270
+ white-space: nowrap;
271
+ color: var(--atx-muted-fg);
272
+ font: 400 14px/1.4 var(--atx-font-ui);
273
+ }
274
+
275
+ /* Spans both rows and sits at the top of them, so a one-line and a two-line
276
+ header put their action in the same place. */
277
+ .atx-drawer-actions {
278
+ display: flex;
279
+ gap: 6px;
280
+ grid-column: 2;
281
+ grid-row: 1 / span 2;
282
+ align-self: start;
283
+ justify-self: end;
284
+ }
285
+
286
+ /* A stack of cards, not a form. The gap is the only spacing the body owns;
287
+ each card brings its own padding, which is what keeps a card's edge a real
288
+ boundary rather than a line drawn through continuous content. */
289
+ .atx-drawer-body {
290
+ display: flex;
291
+ flex-direction: column;
292
+ gap: 16px;
293
+ flex: 1 1 auto;
294
+ padding: 16px;
295
+ overflow-y: auto;
296
+ }
297
+
298
+ /* A band, not a strip of the body: a rule above it and a ground half a step
299
+ off the drawer's own are what separate "the form" from "what you do with
300
+ it", and are the reason the footer survives being scrolled up to. */
301
+ .atx-drawer-foot {
302
+ display: flex;
303
+ align-items: center;
304
+ justify-content: flex-end;
305
+ gap: 8px;
306
+ flex: 0 0 auto;
307
+ padding: 16px 20px;
308
+ border-top: 1px solid var(--atx-border);
309
+ background: var(--atx-card);
310
+ }
311
+
312
+ /* The destructive action goes to the far end, away from the pair the user is
313
+ actually choosing between. Cancel and Save are one decision; Delete is a
314
+ different one, and putting all three in a row invites the wrong click. */
315
+ .atx-drawer-foot > .atx-btn-destructive:first-child,
316
+ .atx-panel-foot > .atx-btn-destructive:first-child {
317
+ margin-right: auto;
318
+ }
319
+
320
+ .atx-backdrop {
321
+ position: fixed;
322
+ inset: 0;
323
+ background: rgba(0, 0, 0, 0.4);
324
+ }
325
+
326
+ /* == Grouping =============================================================
327
+ group.ts. The structural vocabulary every panel builds its content from:
328
+ a card is a bounded concern, a field group is a run of controls answering
329
+ one question, an item is one row of a list, and a separator is the smaller
330
+ claim a card makes about how far apart two things are.
331
+
332
+ These carry no colour of their own beyond a surface and a rule. What they
333
+ own is *distance* -- which is the part a flat run of controls gets wrong
334
+ however well each control is styled. */
335
+
336
+ /* The edge is a 1px inset shadow rather than a border, so a card can sit
337
+ flush inside a padded body without its own border-box changing the width
338
+ its children get. Same trick shadcn uses, same reason. */
339
+ .atx-card {
340
+ display: flex;
341
+ flex-direction: column;
342
+ gap: 16px;
343
+ padding: 16px 0 0;
344
+ border-radius: var(--atx-radius-xl);
345
+ background: var(--atx-card);
346
+ box-shadow: 0 0 0 1px var(--atx-border);
347
+ }
348
+
349
+ /* Horizontal padding lives on the header, the body and the footer rather
350
+ than on the card, which is what lets the footer band and a full-bleed
351
+ list run edge to edge inside it. */
352
+ .atx-card-head {
353
+ display: grid;
354
+ grid-template-columns: 1fr;
355
+ align-items: start;
356
+ gap: 2px 12px;
357
+ padding: 0 16px;
358
+ }
359
+
360
+ .atx-card-head[data-action] {
361
+ grid-template-columns: 1fr auto;
362
+ }
363
+
364
+ .atx-card-title {
365
+ min-width: 0;
366
+ color: var(--atx-foreground);
367
+ font: 500 16px/1.4 var(--atx-font-ui);
368
+ }
369
+
370
+ .atx-card-desc {
371
+ min-width: 0;
372
+ color: var(--atx-muted-fg);
373
+ font: 400 14px/1.45 var(--atx-font-ui);
374
+ }
375
+
376
+ /* Spans both rows and pins to the top-right, so a card with a description and
377
+ one without put their action in the same place. */
378
+ .atx-card-action {
379
+ grid-column: 2;
380
+ grid-row: 1 / span 2;
381
+ align-self: start;
382
+ justify-self: end;
383
+ }
384
+
385
+ .atx-card-body {
386
+ padding: 0 16px;
387
+ }
388
+
389
+ /* The card's own footer band. Matches the drawer's for the same reason: the
390
+ action that completes a thing sits below a rule, on a ground half a step
391
+ off the surface it completes. */
392
+ .atx-card-foot {
393
+ display: flex;
394
+ align-items: center;
395
+ gap: 8px;
396
+ padding: 16px;
397
+ border-top: 1px solid var(--atx-border);
398
+ border-radius: 0 0 var(--atx-radius-xl) var(--atx-radius-xl);
399
+ background: var(--atx-background);
400
+ }
401
+
402
+ .atx-card-foot > .atx-btn-destructive:first-child {
403
+ margin-right: auto;
404
+ }
405
+
406
+ /* Trailing padding on the last slot, so the card's bottom matches the 16px it
407
+ opens with. A footer brings its own, hence the reset. */
408
+ .atx-card > :last-child {
409
+ padding-bottom: 16px;
410
+ }
411
+
412
+ .atx-card > .atx-card-foot:last-child {
413
+ padding-bottom: 16px;
414
+ }
415
+
416
+ /* One question's worth of controls. 20px between fields is wide enough that
417
+ the label of the next one is plainly a new field rather than a second line
418
+ of the last one's help text -- which is the whole job. */
419
+ .atx-field-group {
420
+ display: flex;
421
+ flex-direction: column;
422
+ gap: 20px;
423
+ }
424
+
425
+ .atx-sep {
426
+ height: 1px;
427
+ flex: 0 0 auto;
428
+ background: var(--atx-border);
429
+ }
430
+
431
+ /* A footnote to the title beside it, so it never takes the slack and never
432
+ wraps: whatever it qualifies is the thing allowed to ellipsize.
433
+
434
+ Borrowed from the outline button -- the same 1px input border over the same
435
+ faint fill -- because a chip that reads as chrome sits quietly beside a name,
436
+ where a tone-coloured outline shouted the qualifier louder than the thing it
437
+ qualifies. Muted ink, not foreground: this is text being read, not pressed.
438
+ Radius stays "full", which is the ladder's rule for a badge and the one thing
439
+ keeping it from being mistaken for the button it is coloured like. */
440
+ .atx-badge {
441
+ flex: 0 0 auto;
442
+ padding: 2px 8px;
443
+ border: 1px solid var(--atx-input);
444
+ border-radius: var(--atx-radius-full);
445
+ background: var(--atx-control-bg);
446
+ color: var(--atx-muted-fg);
447
+ font: 500 12px/1.4 var(--atx-font-ui);
448
+ white-space: nowrap;
449
+ }
450
+
451
+ /* The one tone that departs from chrome: something the reader has to act on. */
452
+ .atx-badge[data-tone='warn'] {
453
+ border-color: var(--atx-warning);
454
+ color: var(--atx-warning);
455
+ }
456
+
457
+ /* == Items ================================================================
458
+ One row: an optional 16px media slot, a text column that takes the slack,
459
+ and actions pinned right. Every list in the overlay is built from this
460
+ rather than each panel growing its own row. */
461
+
462
+ /* A list, not a stack of tiles. Rows sit flush and a 1px rule separates them:
463
+ the row already carries a transparent 1px border, so the rule is a colour
464
+ change and costs no layout shift. Gaps would make each row read as its own
465
+ object, which is exactly what a list is not. */
466
+ .atx-item-group {
467
+ display: flex;
468
+ flex-direction: column;
469
+ }
470
+
471
+ .atx-item-group > .atx-item + .atx-item {
472
+ border-top-color: var(--atx-border);
473
+ }
474
+
475
+ /* Edge to edge in the card body holding it, so the rules reach the card's own
476
+ edges and a hover fill covers the whole row rather than an inset tile. The
477
+ 16px it cancels is .atx-card-body's, put back on each row. */
478
+ .atx-item-group[data-bleed] {
479
+ margin: 0 -16px;
480
+ }
481
+
482
+ .atx-item-group[data-bleed] > .atx-item {
483
+ padding-right: 16px;
484
+ padding-left: 16px;
485
+ border-right: 0;
486
+ border-left: 0;
487
+ border-radius: 0;
488
+ }
489
+
490
+ /* .atx-card carries no bottom padding, so a bleed list that ends the card
491
+ also ends the card: its last row has to pick up the card's own corners or a
492
+ square hover fill spills past them. Selector says exactly that condition --
493
+ the card can't be given overflow:hidden instead, which would clip a row's
494
+ focus ring. */
495
+ .atx-card-body:last-child > .atx-item-group[data-bleed]:last-child > .atx-item:last-child {
496
+ border-bottom-right-radius: var(--atx-radius-xl);
497
+ border-bottom-left-radius: var(--atx-radius-xl);
498
+ }
499
+
500
+ .atx-item {
501
+ display: flex;
502
+ align-items: center;
503
+ gap: 10px;
504
+ padding: 10px 12px;
505
+ border: 1px solid transparent;
506
+ border-radius: var(--atx-radius-lg);
507
+ color: var(--atx-foreground);
508
+ font: 400 14px var(--atx-font-ui);
509
+ }
510
+
511
+ /* A filled tile, for a row that stands alone. A row inside a list stays
512
+ transparent: there, the list is the object and painting every row makes it
513
+ read as a stack of separate cards. */
514
+ .atx-item[data-variant='muted'] {
515
+ border-color: var(--atx-border);
516
+ background: var(--atx-elevated);
517
+ }
518
+
519
+ .atx-item-media {
520
+ display: flex;
521
+ align-items: center;
522
+ justify-content: center;
523
+ width: 16px;
524
+ height: 16px;
525
+ flex: 0 0 auto;
526
+ color: var(--atx-muted-fg);
527
+ }
528
+
529
+ .atx-item-content {
530
+ display: flex;
531
+ flex-direction: column;
532
+ gap: 2px;
533
+ min-width: 0;
534
+ flex: 1 1 0;
535
+ }
536
+
537
+ .atx-item-title {
538
+ overflow: hidden;
539
+ text-overflow: ellipsis;
540
+ white-space: nowrap;
541
+ font: 500 14px/1.4 var(--atx-font-ui);
542
+ }
543
+
544
+ /* 12px here, unlike a field's help text at 14px. A description under a row
545
+ title is an attribute of the row -- a count, a path, a date -- not prose
546
+ the user has to read, and at 14px it competes with the title. */
547
+ .atx-item-desc {
548
+ overflow: hidden;
549
+ text-overflow: ellipsis;
550
+ white-space: nowrap;
551
+ color: var(--atx-muted-fg);
552
+ font: 500 12px/1.5 var(--atx-font-ui);
553
+ }
554
+
555
+ .atx-item-actions {
556
+ display: flex;
557
+ align-items: center;
558
+ gap: 8px;
559
+ flex: 0 0 auto;
560
+ }
561
+
562
+ /* Tabs are keyed off their ARIA roles, not their classes: buildTabs names
563
+ those per caller (atx-<prefix>-tab), so there is no single class to match,
564
+ and the roles are already there and already correct. Selected state reads
565
+ aria-selected for the same reason it is set at all — one source of truth
566
+ for "this tab is active", instead of a class shadowing an attribute. */
567
+ /* A segmented control: the strip is the recessed track, and the selected tab
568
+ is a raised card sitting in it. The alternative — a filled accent on the
569
+ selected tab — reads as a call to action, which a tab is not. */
570
+ [role='tablist'] {
571
+ display: inline-flex;
572
+ align-items: center;
573
+ align-self: flex-start;
574
+ gap: 2px;
575
+ width: fit-content;
576
+ max-width: 100%;
577
+ flex: 0 0 auto;
578
+ padding: 3px;
579
+ border-radius: var(--atx-radius-lg);
580
+ background: var(--atx-input-bg);
581
+ }
582
+
583
+ [role='tab'] {
584
+ height: 26px;
585
+ padding: 0 12px;
586
+ border: 1px solid transparent;
587
+ border-radius: var(--atx-radius-md);
588
+ background: transparent;
589
+ color: var(--atx-muted-fg);
590
+ font: 500 13px var(--atx-font-ui);
591
+ white-space: nowrap;
592
+ outline: none;
593
+ cursor: pointer;
594
+ transition: background 150ms, color 150ms;
595
+ }
596
+
597
+ [role='tab']:hover {
598
+ color: var(--atx-foreground);
599
+ }
600
+
601
+ [role='tab'][aria-selected='true'] {
602
+ background: var(--atx-accent);
603
+ color: var(--atx-foreground);
604
+ }
605
+
606
+ [data-tabhost] {
607
+ flex: 1 1 auto;
608
+ min-width: 0;
609
+ min-height: 0;
610
+ }
611
+
612
+ /* == Controls ==============================================================
613
+ Buttons, form controls, the toast, the save veil and the pill button. Three
614
+ of these are keyed off a data attribute rather than a class, because the
615
+ class is chosen by the caller and there is no shared one to match:
616
+ [data-input] for anything built by inputEl(), [data-pill] for pillButton's
617
+ translucent chrome, [data-dimmed] for a button switched off through
618
+ setButtonEnabled. */
619
+
620
+ /* Every button in the overlay, keyed off the five variant classes. A bare
621
+ .atx-btn rule cannot be the base: four leaves borrow the atx-btn hook
622
+ without a variant and carry their own box, and handing them this padding
623
+ and radius would resize them.
624
+
625
+ 32px, and on the same radius rung as a form control, so a button beside a
626
+ field lines up and reads as part of the same object rather than as a
627
+ different kind of thing parked next to it. What separates the two is fill
628
+ and weight, not shape. */
629
+ .atx-btn-default,
630
+ .atx-btn-secondary,
631
+ .atx-btn-outline,
632
+ .atx-btn-ghost,
633
+ .atx-btn-destructive {
634
+ display: inline-flex;
635
+ align-items: center;
636
+ justify-content: center;
637
+ gap: 6px;
638
+ height: 32px;
639
+ padding: 0 10px;
640
+ box-sizing: border-box;
641
+ border-radius: var(--atx-radius-lg);
642
+ font: 500 14px/1 var(--atx-font-ui);
643
+ white-space: nowrap;
644
+ outline: none;
645
+ transition: background 150ms, border-color 150ms, color 150ms, box-shadow 150ms;
646
+ cursor: pointer;
647
+ }
648
+
649
+ /* One size down, for a button that is not the point of the surface it sits on:
650
+ a header's corner action, a row's own control. Smaller in every dimension
651
+ at once -- height, type, gap and radius -- because a button that only loses
652
+ height reads as a squashed full-size button rather than as a lesser one. */
653
+ .atx-btn-sm {
654
+ height: 28px;
655
+ padding: 0 10px;
656
+ gap: 4px;
657
+ border-radius: var(--atx-radius-md);
658
+ font-size: 12.8px;
659
+ }
660
+
661
+ /* Icon-only: a square, so the glyph sits in the middle of it rather than in
662
+ the middle of a label-shaped box that has no label. */
663
+ .atx-btn-icon {
664
+ width: 28px;
665
+ height: 28px;
666
+ padding: 0;
667
+ border-radius: var(--atx-radius-md);
668
+ }
669
+
670
+ /* An icon inside a button is sized to the type, not to the button, and sits
671
+ 6px from its label -- the gap above. */
672
+ .atx-btn-default > .atx-ico,
673
+ .atx-btn-secondary > .atx-ico,
674
+ .atx-btn-outline > .atx-ico,
675
+ .atx-btn-ghost > .atx-ico,
676
+ .atx-btn-destructive > .atx-ico {
677
+ width: 16px;
678
+ height: 16px;
679
+ }
680
+
681
+ /* The press. Cheap, and it is most of what makes a button feel like one. */
682
+ .atx-btn-default:active:not(:disabled),
683
+ .atx-btn-secondary:active:not(:disabled),
684
+ .atx-btn-outline:active:not(:disabled),
685
+ .atx-btn-ghost:active:not(:disabled),
686
+ .atx-btn-destructive:active:not(:disabled) {
687
+ transform: translateY(1px);
688
+ }
689
+
690
+ /* The one emphatic fill. Near-white with dark ink, because on a near-black
691
+ overlay the loudest thing available is light rather than hue. */
692
+ .atx-btn-default {
693
+ border: 1px solid transparent;
694
+ background: var(--atx-primary);
695
+ color: var(--atx-primary-fg);
696
+ }
697
+
698
+ .atx-btn-default:hover:not(:disabled) {
699
+ background: ${hexToRgba(COLOR.primary, 0.9)};
700
+ }
701
+
702
+ .atx-btn-secondary {
703
+ border: 1px solid transparent;
704
+ background: var(--atx-elevated);
705
+ color: var(--atx-foreground);
706
+ }
707
+
708
+ .atx-btn-secondary:hover:not(:disabled) {
709
+ background: ${lift(COLOR.elevated, 10)};
710
+ }
711
+
712
+ /* A body and an edge, which is what shadcn's dark outline button actually is:
713
+ bg-input/30 inside border-input, not a line around nothing. Both halves
714
+ matter and the fill matters more -- with a transparent body this read as
715
+ bare text beside the filled confirm it is paired with, even though the two
716
+ boxes measure the same 32px.
717
+
718
+ The edge is the input token (15%), not border (10%): a separator may sit
719
+ under the 3:1 non-text floor, and a control the user has to see to operate
720
+ is not. Both earlier passes here were wrong in opposite directions -- 40%
721
+ read as a boxed-in field, 10% read as a hairline. */
722
+ .atx-btn-outline {
723
+ border: 1px solid var(--atx-input);
724
+ background: var(--atx-control-bg);
725
+ color: var(--atx-foreground);
726
+ }
727
+
728
+ .atx-btn-outline:hover:not(:disabled) {
729
+ background: var(--atx-accent);
730
+ }
731
+
732
+ /* The quiet variant, and the only one with no boundary at rest -- so it is
733
+ restricted to a control that sits *inside* another surface and is read as
734
+ part of it: a header's corner action, a toolbar key, a back link. A footer
735
+ action is never ghost. A footer is a band of decisions with nothing else in
736
+ it, and a label floating in one with no box and no edge does not read as a
737
+ button at all; that is what the outline variant is for.
738
+
739
+ Full-strength ink, not muted. Muted is for text being *read*, and a control
740
+ painted in it looks disabled before it looks quiet. */
741
+ .atx-btn-ghost {
742
+ border: 1px solid transparent;
743
+ background: transparent;
744
+ color: var(--atx-foreground);
745
+ }
746
+
747
+ .atx-btn-ghost:hover:not(:disabled) {
748
+ background: var(--atx-accent);
749
+ color: var(--atx-foreground);
750
+ }
751
+
752
+ /* margin-right: auto pushes a destructive button to the far left of a flex
753
+ footer, away from the safe actions.
754
+
755
+ Deliberately an outline where shadcn fills it: a footer that puts a solid
756
+ red beside the solid confirm button reads as two equal calls to action when
757
+ only one of them is the thing the user came to do. The edge is the danger
758
+ colour at 40%, so it states itself without shouting. */
759
+ .atx-btn-destructive {
760
+ margin-right: auto;
761
+ border: 1px solid ${hexToRgba(COLOR.destructive, 0.4)};
762
+ background: transparent;
763
+ color: var(--atx-destructive);
764
+ }
765
+
766
+ .atx-btn-destructive:hover:not(:disabled) {
767
+ border-color: ${hexToRgba(COLOR.destructive, 0.7)};
768
+ background: ${hexToRgba(COLOR.destructive, 0.1)};
769
+ }
770
+
771
+ /* One dimmed population, not two. setButtonEnabled dims through [data-dimmed]
772
+ and a dozen callers set .disabled directly; a button that is off should look
773
+ off however it got there, or it reads as broken rather than unavailable. */
774
+ [data-dimmed],
775
+ button:disabled,
776
+ [data-input]:disabled {
777
+ opacity: 0.5;
778
+ cursor: not-allowed;
779
+ }
780
+
781
+ /* -- Focus -----------------------------------------------------------------
782
+ :focus-visible, never :focus -- the ring is for the keyboard, and painting
783
+ it on every mouse click is what makes people turn focus indicators off.
784
+
785
+ It carries more weight here than in most designs. A control's resting border
786
+ is transparent, so the ring is not a nicety on top of an outline that is
787
+ already there: it *is* the non-text indication. That is why it is 3px, why
788
+ the ring token is held to 3:1 on every surface, and why the border it paints
789
+ belongs to the same rule. */
790
+ .atx-btn-default:focus-visible,
791
+ .atx-btn-secondary:focus-visible,
792
+ .atx-btn-outline:focus-visible,
793
+ .atx-btn-ghost:focus-visible,
794
+ .atx-btn-destructive:focus-visible,
795
+ [data-input]:focus-visible,
796
+ [role='tab']:focus-visible {
797
+ border-color: var(--atx-ring);
798
+ box-shadow: 0 0 0 3px ${hexToRgba(COLOR.ring, 0.3)};
799
+ }
800
+
801
+ .atx-btn-destructive:focus-visible {
802
+ border-color: var(--atx-destructive);
803
+ box-shadow: 0 0 0 3px ${hexToRgba(COLOR.destructive, 0.4)};
804
+ }
805
+
806
+ /* -- Form controls ---------------------------------------------------------
807
+ Inputs, textareas and selects, keyed off [data-input] rather than a class
808
+ because every caller names its own (atx-field-input, atx-collections-input,
809
+ atx-settings-key ...) and there is no shared class to match.
810
+
811
+ **A control has no border at rest.** Its edge is where the fill stops, and
812
+ the 1px border is transparent, held in reserve for focus and for an invalid
813
+ value -- which is why those two states read as strongly as they do. Adding a
814
+ solid resting outline is the obvious change to make here, and it is the one
815
+ that would undo the look; see COLOR.input for the trade that buys.
816
+
817
+ color-scheme: dark keeps the browser's own chrome -- the date picker's
818
+ calendar popup, number spinners -- light rather than a near-invisible dark
819
+ glyph on a dark field. */
820
+ [data-input] {
821
+ width: 100%;
822
+ min-height: 32px;
823
+ padding: 4px 10px;
824
+ box-sizing: border-box;
825
+ border: 1px solid transparent;
826
+ border-radius: var(--atx-radius-lg);
827
+ background: var(--atx-input-bg);
828
+ color: var(--atx-foreground);
829
+ font: 400 14px/1.45 var(--atx-font-ui);
830
+ outline: none;
831
+ transition: color 200ms, background 200ms, border-color 200ms, box-shadow 200ms;
832
+ color-scheme: dark;
833
+ }
834
+
835
+ /* field-sizing grows the box with the prose in it; min-height is the floor for
836
+ browsers without it, so the fallback is a fixed textarea rather than a
837
+ collapsed one. The extra horizontal padding keeps the text clear of the
838
+ corner curve, which a 64px-tall box has plenty of. */
839
+ textarea[data-input] {
840
+ min-height: 64px;
841
+ padding: 8px 12px;
842
+ line-height: 1.55;
843
+ resize: vertical;
844
+ }
845
+
846
+ /* The browser's own dropdown arrow is drawn hard against the right edge, at
847
+ whatever weight and size the platform picked, so it sits outside our padding
848
+ and looks nothing like the 16px icons everywhere else in the overlay. We
849
+ draw our own instead: appearance:none removes theirs, and the chevron is a
850
+ background image inset 10px from the edge with the text padded clear of it.
851
+
852
+ The glyph's colour is baked in from COLOR.mutedFg rather than read from
853
+ --atx-muted-fg, because a data: URI cannot see a custom property. It is the
854
+ one token a theme override will not move; it is chrome on a control, not
855
+ content, so the trade is worth the alignment. */
856
+ select[data-input] {
857
+ height: 32px;
858
+ padding-right: 32px;
859
+ appearance: none;
860
+ -webkit-appearance: none;
861
+ background-image: ${chevronUrl(COLOR.mutedFg)};
862
+ background-repeat: no-repeat;
863
+ background-position: right 10px center;
864
+ background-size: 16px 16px;
865
+ cursor: pointer;
866
+ }
867
+
868
+ select[data-input]:hover:not(:disabled) {
869
+ background-image: ${chevronUrl(COLOR.foreground)};
870
+ }
871
+
872
+ /* The fill is the control, so the fill is what lifts under the pointer.
873
+ background-color, not the shorthand: the shorthand would drop the chevron a
874
+ select paints as its background-image. */
875
+ [data-input]:hover:not(:disabled):not(:focus) {
876
+ background-color: rgb(255 255 255 / 0.11);
877
+ }
878
+
879
+ [data-input]::placeholder {
880
+ color: var(--atx-muted-fg);
881
+ }
882
+
883
+ /* The selection inside a field is the loud colour, so a selected run reads as
884
+ selected against a surface that is itself already light. */
885
+ [data-input]::selection {
886
+ background: var(--atx-primary);
887
+ color: var(--atx-primary-fg);
888
+ }
889
+
890
+ /* Set by fields.ts alongside the error line, so the boundary and the message
891
+ appear together -- and so a screen reader is told, which the red border on
892
+ its own never did. This is one of the two states the reserved border exists
893
+ for. */
894
+ [data-input][aria-invalid='true'] {
895
+ border-color: var(--atx-destructive);
896
+ }
897
+
898
+ [data-input][aria-invalid='true']:focus-visible {
899
+ border-color: var(--atx-destructive);
900
+ box-shadow: 0 0 0 3px ${hexToRgba(COLOR.destructive, 0.4)};
901
+ }
902
+
903
+ /* -- Checkbox --------------------------------------------------------------
904
+ Drawn rather than tinted. accent-color can only recolour the browser's own
905
+ box, and the browser's box is a 2px-radius outlined square: the wrong shape
906
+ whatever colour it is. appearance:none takes the painting and leaves every
907
+ keyboard and assistive behaviour exactly where it was.
908
+
909
+ Unchecked it is the full-strength input fill with no border, matching the
910
+ larger controls; checked it is the same near-white as the confirm button,
911
+ with the tick masked out of it. */
912
+ input[type='checkbox'] {
913
+ appearance: none;
914
+ -webkit-appearance: none;
915
+ display: inline-grid;
916
+ place-content: center;
917
+ width: 16px;
918
+ height: 16px;
919
+ margin: 0;
920
+ flex: 0 0 auto;
921
+ box-sizing: border-box;
922
+ border: 1px solid transparent;
923
+ border-radius: 4px;
924
+ background: var(--atx-input);
925
+ outline: none;
926
+ transition: background 150ms, border-color 150ms, box-shadow 150ms;
927
+ cursor: pointer;
928
+ }
929
+
930
+ input[type='checkbox']::before {
931
+ content: '';
932
+ width: 12px;
933
+ height: 12px;
934
+ background: var(--atx-primary-fg);
935
+ transform: scale(0);
936
+ transition: transform 120ms ease-out;
937
+ -webkit-mask: ${CHECK_MASK};
938
+ mask: ${CHECK_MASK};
939
+ }
940
+
941
+ input[type='checkbox']:checked {
942
+ border-color: var(--atx-primary);
943
+ background: var(--atx-primary);
944
+ }
945
+
946
+ input[type='checkbox']:checked::before {
947
+ transform: scale(1);
948
+ }
949
+
950
+ input[type='checkbox']:focus-visible {
951
+ border-color: var(--atx-ring);
952
+ box-shadow: 0 0 0 3px ${hexToRgba(COLOR.ring, 0.3)};
953
+ }
954
+
955
+ /* == Switch =================================================================
956
+ ui.ts::switchControl. The same input[type=checkbox] underneath, repainted as
957
+ a track and a thumb — so every rule above has to be overridden here, and the
958
+ selector carries the type and the attribute as well as the class to outrank
959
+ it rather than relying on source order.
960
+
961
+ Geometry is shadcn's, measured off the live reference: a 32x18 track and 14px
962
+ of travel. The thumb is 14px inset 2px rather than the reference's 16px inset
963
+ 1px -- at this track height a 1px inset survives on the flat top and bottom
964
+ but is eaten by the pill's corner curve at each end, so the checked thumb
965
+ reads as touching the track. 2px is the smallest inset that still shows at
966
+ the ends, and the travel is unchanged (32 - 14 - 2 - 2). The colours are this overlay's own: the
967
+ unchecked track is the full-strength input fill every small control is made
968
+ of, and the checked track is the near-white primary that the confirm button
969
+ and a ticked checkbox already wear -- the brand purple the reference uses
970
+ here is spoken for, and means "this element on your page is editable". */
971
+ .atx-switch-row {
972
+ display: inline-flex;
973
+ align-items: center;
974
+ gap: 8px;
975
+ flex: 0 0 auto;
976
+ cursor: pointer;
977
+ }
978
+
979
+ .atx-switch-label {
980
+ color: var(--atx-muted-fg);
981
+ font: 500 12px/1.4 var(--atx-font-ui);
982
+ user-select: none;
983
+ }
984
+
985
+ input[type='checkbox'].atx-switch {
986
+ position: relative;
987
+ display: inline-flex;
988
+ place-content: unset;
989
+ align-items: center;
990
+ width: 32px;
991
+ height: 18px;
992
+ padding: 0;
993
+ border-radius: var(--atx-radius-full);
994
+ background: var(--atx-input);
995
+ }
996
+
997
+ /* The thumb. ::before is the checkbox's tick slot, reused — there is one
998
+ pseudo-element to spend and a switch has one moving part. */
999
+ input[type='checkbox'].atx-switch::before {
1000
+ content: '';
1001
+ position: absolute;
1002
+ /* Against the padding box, inside the 1px transparent border the shared
1003
+ checkbox rule gives every box: 1px here is the 2px the thumb reads as. */
1004
+ left: 1px;
1005
+ width: 14px;
1006
+ height: 14px;
1007
+ border-radius: var(--atx-radius-full);
1008
+ background: var(--atx-foreground);
1009
+ transform: none;
1010
+ transition: transform 150ms ease-out, background 150ms;
1011
+ -webkit-mask: none;
1012
+ mask: none;
1013
+ }
1014
+
1015
+ input[type='checkbox'].atx-switch:checked {
1016
+ border-color: transparent;
1017
+ background: var(--atx-primary);
1018
+ }
1019
+
1020
+ /* Travel = track - thumb - both insets. Moving the thumb rather than animating
1021
+ left keeps it on the compositor. */
1022
+ input[type='checkbox'].atx-switch:checked::before {
1023
+ background: var(--atx-primary-fg);
1024
+ transform: translateX(14px);
1025
+ }
1026
+
1027
+ /* Disabled is handled here rather than inherited: the shared [data-input]
1028
+ baseline this control opts out of is what usually carries it. The label dims
1029
+ with the track, so the pair reads as one thing switched off. */
1030
+ input[type='checkbox'].atx-switch:disabled {
1031
+ opacity: 0.5;
1032
+ cursor: not-allowed;
1033
+ }
1034
+
1035
+ .atx-switch-row:has(input:disabled) {
1036
+ cursor: not-allowed;
1037
+ }
1038
+
1039
+ .atx-switch-row:has(input:disabled) .atx-switch-label {
1040
+ opacity: 0.5;
1041
+ }
1042
+
1043
+ [data-pill] {
1044
+ display: inline-flex;
1045
+ align-items: center;
1046
+ justify-content: center;
1047
+ gap: 4px;
1048
+ margin-left: 8px;
1049
+ padding: 2px 7px;
1050
+ border: none;
1051
+ border-radius: var(--atx-radius-sm);
1052
+ background: rgba(255, 255, 255, 0.14);
1053
+ color: var(--atx-foreground);
1054
+ font: 600 11px var(--atx-font-ui);
1055
+ cursor: pointer;
1056
+ }
1057
+
1058
+ [data-pill]:hover {
1059
+ background: rgba(255, 255, 255, 0.28);
1060
+ }
1061
+
1062
+ /* Optical centring, which flexbox cannot do for text — see pillButton in
1063
+ ui.ts for why an 11px lowercase label reads as sitting low without it. */
1064
+ .atx-pill-label {
1065
+ position: relative;
1066
+ top: -1.5px;
1067
+ }
1068
+
1069
+ .atx-toast {
1070
+ position: fixed;
1071
+ left: 50%;
1072
+ transform: translateX(-50%);
1073
+ padding: 10px 18px;
1074
+ border-radius: var(--atx-radius-lg);
1075
+ font: 500 14px var(--atx-font-ui);
1076
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.32);
1077
+ opacity: 0;
1078
+ transition: opacity 120ms;
1079
+ }
1080
+
1081
+ .atx-toast[data-shown] {
1082
+ opacity: 1;
1083
+ }
1084
+
1085
+ .atx-toast-ok {
1086
+ background: var(--atx-success);
1087
+ color: var(--atx-foreground);
1088
+ }
1089
+
1090
+ .atx-toast-err {
1091
+ background: var(--atx-destructive);
1092
+ color: var(--atx-primary-fg);
1093
+ }
1094
+
1095
+ /* The save veil's rect is measured off a host element, so its geometry is the
1096
+ one thing that stays inline. */
1097
+ .atx-veil {
1098
+ position: fixed;
1099
+ display: flex;
1100
+ align-items: center;
1101
+ justify-content: center;
1102
+ border-radius: var(--atx-radius-sm);
1103
+ background: ${hexToRgba(COLOR.brand, 0.12)};
1104
+ pointer-events: all;
1105
+ }
1106
+
1107
+ .atx-veil-chip {
1108
+ padding: 2px 8px;
1109
+ border-radius: var(--atx-radius-full);
1110
+ background: var(--atx-brand);
1111
+ color: var(--atx-foreground);
1112
+ font: 600 11px var(--atx-font-ui);
1113
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
1114
+ }
1115
+
1116
+ /* == Hover pill and outline ================================================
1117
+ hover.ts. The outline and the pill are shown by a [data-on] flag rather than
1118
+ an inline display. The verdict colours the outline alone -- its border and
1119
+ fill stay inline, because that colour is chosen per element at hover time.
1120
+ The pill carries no verdict colour: it says the word, the box round the
1121
+ element carries the hue. */
1122
+
1123
+ /* The measured rect is inset by OUTLINE_GAP in hover.ts so the border clears
1124
+ the element's own edge instead of sitting flush against its ink. */
1125
+ .atx-outline {
1126
+ position: fixed;
1127
+ z-index: ${Z};
1128
+ display: none;
1129
+ border: 2px solid var(--atx-brand);
1130
+ border-radius: var(--atx-radius-md);
1131
+ background: ${hexToRgba(COLOR.brand, 0.08)};
1132
+ pointer-events: none;
1133
+ transition: all 60ms ease-out;
1134
+ }
1135
+
1136
+ .atx-outline[data-on] {
1137
+ display: block;
1138
+ }
1139
+
1140
+ /* The pill is interactive: hovering it keeps it open, and its "open source"
1141
+ button jumps to the element's source in the editor. */
1142
+ .atx-tooltip {
1143
+ position: fixed;
1144
+ z-index: ${Z + 1};
1145
+ display: none;
1146
+ /* Column so the loc line and the class/ID chips row stack; each row sizes
1147
+ to its own content (flex-start) rather than stretching to the widest. */
1148
+ flex-direction: column;
1149
+ align-items: flex-start;
1150
+ /* Concentric with the pill buttons inside it: their 6px radius plus the 4px
1151
+ of padding around them is the 10px this curve needs to read as wrapping
1152
+ them rather than being clipped by them. The left padding is bigger because
1153
+ what sits there is a text label, which has no box of its own to inset. */
1154
+ padding: 4px 4px 4px 10px;
1155
+ border-radius: var(--atx-radius-lg);
1156
+ background: var(--atx-card);
1157
+ color: var(--atx-foreground);
1158
+ font: 500 12px/1.4 var(--atx-font-mono);
1159
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
1160
+ white-space: nowrap;
1161
+ pointer-events: auto;
1162
+ cursor: default;
1163
+ }
1164
+
1165
+ .atx-tooltip[data-on] {
1166
+ display: flex;
1167
+ }
1168
+
1169
+ .atx-tooltip-row {
1170
+ display: flex;
1171
+ align-items: center;
1172
+ }
1173
+
1174
+ /* The label is clickable too — the whole file:loc line opens the in-browser
1175
+ source peek (the "open" button next to it is the editor jump). */
1176
+ .atx-tooltip-label {
1177
+ cursor: pointer;
1178
+ }
1179
+
1180
+ .atx-tooltip-label:hover {
1181
+ text-decoration: underline;
1182
+ }
1183
+
1184
+ /* The verdict sits in a fixed-width slot: the pill must not resize
1185
+ (distracting) when "loading…" upgrades to the real verdict. 8ch fits the
1186
+ longest words in the pill's monospace font; shorter verdicts leave a little
1187
+ slack instead of shrinking the pill. */
1188
+ .atx-tooltip-verdict {
1189
+ display: inline-block;
1190
+ width: 8ch;
1191
+ }
1192
+
1193
+ .atx-tooltip-chips {
1194
+ display: none;
1195
+ flex-wrap: wrap;
1196
+ gap: 4px;
1197
+ max-width: 340px;
1198
+ margin-top: 5px;
1199
+ padding-top: 5px;
1200
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
1201
+ white-space: normal;
1202
+ }
1203
+
1204
+ .atx-tooltip-chips[data-on] {
1205
+ display: flex;
1206
+ }
1207
+
1208
+ .atx-tooltip-chip {
1209
+ padding: 1px 6px;
1210
+ border: 1px solid transparent;
1211
+ border-radius: var(--atx-radius-sm);
1212
+ background: rgba(255, 255, 255, 0.09);
1213
+ color: var(--atx-muted-fg);
1214
+ font: 500 11px var(--atx-font-mono);
1215
+ cursor: default;
1216
+ }
1217
+
1218
+ /* Hovering a chip also pops its rules card; that half stays in JS. */
1219
+ .atx-tooltip-chip:hover {
1220
+ border-color: var(--atx-brand);
1221
+ }
1222
+
1223
+ /* == Element tree ==========================================================
1224
+ tree.ts. Its panel, its edge tab and the locked-selection outline are all
1225
+ shown by [data-on]; what stays inline is the panel's top/bottom (the admin
1226
+ bar reserves a strip of one edge and reports it through onChromeInset), a
1227
+ row's indent, and the selection's measured rect. */
1228
+
1229
+ .atx-tree {
1230
+ position: fixed;
1231
+ left: 5px;
1232
+ /* Top/bottom rather than a height: the admin bar reserves a strip of one
1233
+ edge, and the panel must never sit under it. Both are overwritten inline
1234
+ as the inset changes. */
1235
+ top: 5px;
1236
+ bottom: 5px;
1237
+ z-index: ${Z + 3};
1238
+ display: none;
1239
+ flex-direction: column;
1240
+ width: min(320px, 90vw);
1241
+ box-sizing: border-box;
1242
+ border: 1px solid var(--atx-border);
1243
+ border-radius: var(--atx-radius-xl);
1244
+ background: rgba(0, 0, 0, 0.9);
1245
+ color: var(--atx-muted-fg);
1246
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.4);
1247
+ font: 400 12px/1.5 var(--atx-font-mono);
1248
+ /* Edit mode sets a page-wide crosshair; the panel is not click-to-edit. */
1249
+ cursor: auto;
1250
+ }
1251
+
1252
+ .atx-tree[data-on] {
1253
+ display: flex;
1254
+ }
1255
+
1256
+ .atx-tree-title {
1257
+ display: flex;
1258
+ align-items: center;
1259
+ gap: 8px;
1260
+ flex: 0 0 auto;
1261
+ padding: 14px 16px;
1262
+ border-bottom: 1px solid var(--atx-border);
1263
+ color: var(--atx-foreground);
1264
+ font: 500 14px var(--atx-font-ui);
1265
+ }
1266
+
1267
+ .atx-tree-title-text {
1268
+ flex: 1 1 auto;
1269
+ }
1270
+
1271
+ .atx-tree-close {
1272
+ display: inline-flex;
1273
+ align-items: center;
1274
+ justify-content: center;
1275
+ flex: 0 0 auto;
1276
+ width: 24px;
1277
+ height: 24px;
1278
+ padding: 0;
1279
+ border: none;
1280
+ border-radius: var(--atx-radius-md);
1281
+ background: transparent;
1282
+ color: var(--atx-muted-fg);
1283
+ cursor: pointer;
1284
+ }
1285
+
1286
+ .atx-tree-close:hover {
1287
+ background: var(--atx-accent);
1288
+ color: var(--atx-foreground);
1289
+ }
1290
+
1291
+ /* The edge tab that reopens the tree. Only meaningful while editing — outside
1292
+ edit mode the tree has nothing live to point at. */
1293
+ .atx-tree-tab {
1294
+ position: fixed;
1295
+ left: 0;
1296
+ top: 50%;
1297
+ transform: translateY(-50%);
1298
+ z-index: ${Z + 3};
1299
+ display: none;
1300
+ align-items: center;
1301
+ justify-content: center;
1302
+ width: 24px;
1303
+ height: 64px;
1304
+ padding: 0;
1305
+ border: none;
1306
+ border-right: 1px solid rgba(255, 255, 255, 0.1);
1307
+ border-radius: 0 7px 7px 0;
1308
+ background: ${hexToRgba(COLOR.glass, 0.88)};
1309
+ backdrop-filter: blur(10px);
1310
+ color: var(--atx-brand-text);
1311
+ box-shadow: 2px 0 14px rgba(0, 0, 0, 0.3);
1312
+ cursor: pointer;
1313
+ }
1314
+
1315
+ .atx-tree-tab[data-on] {
1316
+ display: flex;
1317
+ }
1318
+
1319
+ .atx-tree-tab:hover {
1320
+ color: var(--atx-foreground);
1321
+ }
1322
+
1323
+ .atx-tree-body {
1324
+ flex: 1 1 auto;
1325
+ padding: 6px 0;
1326
+ overflow-y: auto;
1327
+ overflow-x: hidden;
1328
+ }
1329
+
1330
+ .atx-tree-selection {
1331
+ position: fixed;
1332
+ z-index: ${Z};
1333
+ display: none;
1334
+ border: 2px solid var(--atx-brand);
1335
+ border-radius: var(--atx-radius-md);
1336
+ box-shadow: 0 0 0 2px ${COLOR.brand}44, 0 0 12px ${COLOR.brand}66;
1337
+ pointer-events: none;
1338
+ }
1339
+
1340
+ .atx-tree-selection[data-on] {
1341
+ display: block;
1342
+ }
1343
+
1344
+ /* A row's indent is its depth, so padding-left stays inline. */
1345
+ .atx-tree-row {
1346
+ display: flex;
1347
+ align-items: center;
1348
+ gap: 5px;
1349
+ padding: 3px 10px 3px 0;
1350
+ border-radius: var(--atx-radius-md);
1351
+ background: transparent;
1352
+ color: var(--atx-muted-fg);
1353
+ outline: none;
1354
+ outline-offset: -1px;
1355
+ white-space: nowrap;
1356
+ cursor: pointer;
1357
+ }
1358
+
1359
+ /* Surface shift, not a tint — every colour in the tree means "this is where
1360
+ your element is", so hover must not borrow one. */
1361
+ .atx-tree-row:hover:not([data-state]) {
1362
+ background: var(--atx-accent);
1363
+ color: var(--atx-foreground);
1364
+ }
1365
+
1366
+ /* Hover-active reads as a dashed ring so it never looks like the solid locked
1367
+ selection, even when both land on the same row. The brand tint here is one
1368
+ of the places the purple keeps its meaning — it points at your page. */
1369
+ .atx-tree-row[data-state='active'] {
1370
+ background: ${hexToRgba(COLOR.brand, 0.16)};
1371
+ color: var(--atx-brand-text);
1372
+ outline: 1px dashed var(--atx-brand);
1373
+ }
1374
+
1375
+ .atx-tree-row[data-state='selected'] {
1376
+ background: var(--atx-brand);
1377
+ color: var(--atx-foreground);
1378
+ outline: none;
1379
+ }
1380
+
1381
+ .atx-tree-chevron {
1382
+ display: inline-flex;
1383
+ align-items: center;
1384
+ justify-content: center;
1385
+ flex: 0 0 auto;
1386
+ width: 12px;
1387
+ color: var(--atx-muted-fg);
1388
+ cursor: pointer;
1389
+ }
1390
+
1391
+ .atx-tree-chevron[data-leaf] {
1392
+ cursor: default;
1393
+ }
1394
+
1395
+ /* The tag is what the row *is*; the preview and the loc are what it happens to
1396
+ contain and where it happens to live. Full-strength ink on the tag against
1397
+ the row's muted default is what makes a long tree scannable by shape -- the
1398
+ angle brackets included, since they are what say "element" at a glance. */
1399
+ .atx-tree-tag {
1400
+ flex: 0 0 auto;
1401
+ color: var(--atx-foreground);
1402
+ font-weight: 600;
1403
+ }
1404
+
1405
+ .atx-tree-preview {
1406
+ flex: 0 1 auto;
1407
+ overflow: hidden;
1408
+ text-overflow: ellipsis;
1409
+ color: var(--atx-muted-fg);
1410
+ }
1411
+
1412
+ .atx-tree-loc {
1413
+ flex: 0 0 auto;
1414
+ margin-left: auto;
1415
+ padding-left: 10px;
1416
+ color: var(--atx-faint-fg);
1417
+ font-size: 11px;
1418
+ font-variant-numeric: tabular-nums;
1419
+ text-decoration: underline dotted transparent;
1420
+ text-underline-offset: 2px;
1421
+ cursor: pointer;
1422
+ }
1423
+
1424
+ .atx-tree-loc:hover {
1425
+ color: var(--atx-brand-text);
1426
+ text-decoration-color: var(--atx-brand-text);
1427
+ }
1428
+
1429
+ .atx-tree-empty {
1430
+ padding: 14px;
1431
+ color: var(--atx-muted-fg);
1432
+ font: 400 13px var(--atx-font-ui);
1433
+ }
1434
+
1435
+ /* == Admin bar =============================================================
1436
+ admin-bar.ts. Three state flags drive everything the bar does with itself:
1437
+ [data-edge] which edge it is docked to, [data-open] whether it is revealed,
1438
+ and [data-docked] whether it is in the mode that never retracts (pinned, or
1439
+ edit mode, where the bar carries the save state and the way out and so must
1440
+ never be off-screen). What stays inline is the menu's measured position and
1441
+ the save button's colours, which come from a caller-supplied paint hook
1442
+ rather than a fixed vocabulary. */
1443
+
1444
+ .atx-bar {
1445
+ position: fixed;
1446
+ left: 0;
1447
+ right: 0;
1448
+ height: ${BAR_H}px;
1449
+ z-index: ${Z + 4};
1450
+ display: flex;
1451
+ align-items: center;
1452
+ gap: 6px;
1453
+ padding: 0 8px;
1454
+ box-sizing: border-box;
1455
+ /* A bar spans its edge from corner to corner, so it has no corners of its
1456
+ own to round. Stated rather than left to the default, because every other
1457
+ surface in here carries a radius and this one must not pick one up. */
1458
+ border-radius: 0;
1459
+ /* Opaque enough to guarantee the bar's own contrast. At 0.78 a white page
1460
+ showed through to an effective #494853, which dropped the hint and menu
1461
+ inks to ~3:1; the blur still reads as glass at 0.94, and over a dark page
1462
+ (the common case) the two are indistinguishable. */
1463
+ background: ${hexToRgba(COLOR.glass, 0.94)};
1464
+ backdrop-filter: blur(12px) saturate(1.3);
1465
+ color: var(--atx-foreground);
1466
+ font: 500 13px var(--atx-font-ui);
1467
+ transform: none;
1468
+ opacity: 1;
1469
+ pointer-events: auto;
1470
+ transition: opacity 140ms ease, transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
1471
+ /* Edit mode sets a page-wide crosshair; the bar is not click-to-edit. */
1472
+ cursor: auto;
1473
+ }
1474
+
1475
+ .atx-bar[data-edge='top'] {
1476
+ top: 0;
1477
+ border-bottom: 1px solid rgba(255, 255, 255, 0.09);
1478
+ box-shadow: 0 6px 22px rgba(0, 0, 0, 0.26);
1479
+ }
1480
+
1481
+ .atx-bar[data-edge='bottom'] {
1482
+ bottom: 0;
1483
+ border-top: 1px solid rgba(255, 255, 255, 0.09);
1484
+ box-shadow: 0 -6px 22px rgba(0, 0, 0, 0.26);
1485
+ }
1486
+
1487
+ /* Resting but never in the way — and still readable. At 0.5 the bar's own
1488
+ labels composited down to 2.8:1 against a white page, so the resting bar was
1489
+ the least legible thing the overlay drew. 0.72 keeps it recessive without
1490
+ going back there.
1491
+
1492
+ The resting bar sits near the AA line, and only the resting bar. Over a
1493
+ near-white page a label straight on the bar surface computes to ~5.9:1, and
1494
+ the three that sit inside a chip to ~4.8:1 — the chip's white tint lifts the
1495
+ surface under the ink, which is what makes those three the worst case.
1496
+ Approaching the bar or entering edit mode takes it to opacity 1 and 11:1 or
1497
+ better, which is every state a user reads it in for longer than a glance.
1498
+
1499
+ Those are computed figures: they model opacity as the group buffer it is,
1500
+ but not backdrop-filter's saturate pass, so treat them as ±0.3 and
1501
+ re-measure in the browser — docs/VERIFICATION.md carries that check.
1502
+ Recessive-until-touched is the point of the surface, so the number stays
1503
+ where it is. Tracked as a deferral on the roadmap board, not a bug, and it
1504
+ is why tests/contrast.test.ts pins the tokens rather than this composite,
1505
+ which depends on the host page. */
1506
+ .atx-bar[data-docked]:not([data-open]) {
1507
+ opacity: 0.72;
1508
+ }
1509
+
1510
+ /* Unpinned and unrevealed: off the edge entirely, and not swallowing clicks on
1511
+ the strip it used to cover. */
1512
+ .atx-bar:not([data-docked]):not([data-open]) {
1513
+ opacity: 0;
1514
+ pointer-events: none;
1515
+ }
1516
+
1517
+ .atx-bar:not([data-docked]):not([data-open])[data-edge='top'] {
1518
+ transform: translateY(-100%);
1519
+ }
1520
+
1521
+ .atx-bar:not([data-docked]):not([data-open])[data-edge='bottom'] {
1522
+ transform: translateY(100%);
1523
+ }
1524
+
1525
+ /* The sliver left behind by a retracted bar, so the edge is still findable. */
1526
+ .atx-hairline {
1527
+ position: fixed;
1528
+ left: 0;
1529
+ right: 0;
1530
+ height: 3px;
1531
+ z-index: ${Z + 3};
1532
+ display: none;
1533
+ background: linear-gradient(90deg, transparent, ${COLOR.brand}88, transparent);
1534
+ pointer-events: none;
1535
+ }
1536
+
1537
+ .atx-hairline[data-on] {
1538
+ display: block;
1539
+ }
1540
+
1541
+ .atx-hairline[data-edge='top'] {
1542
+ top: 0;
1543
+ }
1544
+
1545
+ .atx-hairline[data-edge='bottom'] {
1546
+ bottom: 0;
1547
+ }
1548
+
1549
+ .atx-hairline-nub {
1550
+ position: absolute;
1551
+ left: 50%;
1552
+ transform: translateX(-50%);
1553
+ width: 54px;
1554
+ height: 3px;
1555
+ background: var(--atx-brand);
1556
+ }
1557
+
1558
+ .atx-hairline[data-edge='top'] .atx-hairline-nub {
1559
+ top: 0;
1560
+ border-radius: 0 0 3px 3px;
1561
+ }
1562
+
1563
+ .atx-hairline[data-edge='bottom'] .atx-hairline-nub {
1564
+ bottom: 0;
1565
+ border-radius: 3px 3px 0 0;
1566
+ }
1567
+
1568
+ .atx-bar-group {
1569
+ display: flex;
1570
+ align-items: center;
1571
+ gap: 6px;
1572
+ min-width: 0;
1573
+ }
1574
+
1575
+ .atx-bar-group-right {
1576
+ margin-left: auto;
1577
+ min-width: auto;
1578
+ }
1579
+
1580
+ /* The launcher glyph — the one piece of bar furniture that keeps the brand
1581
+ colour, because a product needs one place to be itself. */
1582
+ .atx-bar-brand {
1583
+ display: flex;
1584
+ align-items: center;
1585
+ justify-content: center;
1586
+ flex: 0 0 auto;
1587
+ width: 28px;
1588
+ height: 28px;
1589
+ padding: 0;
1590
+ border: none;
1591
+ border-radius: var(--atx-radius-md);
1592
+ background: var(--atx-brand);
1593
+ color: var(--atx-foreground);
1594
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
1595
+ cursor: pointer;
1596
+ }
1597
+
1598
+ .atx-bar-brand:hover {
1599
+ background: ${lift(COLOR.brand)};
1600
+ }
1601
+
1602
+ .atx-bar-sep {
1603
+ flex: 0 0 auto;
1604
+ width: 1px;
1605
+ height: 18px;
1606
+ margin: 0 3px;
1607
+ background: rgba(255, 255, 255, 0.14);
1608
+ }
1609
+
1610
+ /* The bar's primary ink rather than muted: the resting bar is already at
1611
+ reduced opacity, which dims whatever ink sits on it, and a hint is a message
1612
+ to be read. Its 10.5px size is what keeps it secondary. */
1613
+ .atx-bar-hint {
1614
+ display: none;
1615
+ padding-right: 4px;
1616
+ color: var(--atx-foreground);
1617
+ font: 500 10.5px/1 var(--atx-font-ui);
1618
+ white-space: nowrap;
1619
+ }
1620
+
1621
+ .atx-bar-hint[data-on] {
1622
+ display: inline;
1623
+ }
1624
+
1625
+ .atx-bar-btn {
1626
+ display: inline-flex;
1627
+ align-items: center;
1628
+ justify-content: center;
1629
+ gap: 6px;
1630
+ flex: 0 0 auto;
1631
+ /* 28px -- the small rung of the button ladder, shared with the launcher and
1632
+ the icon buttons so everything in the bar is one height, and the icon
1633
+ button (28px wide) is actually square. */
1634
+ height: 28px;
1635
+ width: auto;
1636
+ padding: 0 11px;
1637
+ border: none;
1638
+ border-radius: var(--atx-radius-md);
1639
+ background: ${BAR_CHIP};
1640
+ color: var(--atx-foreground);
1641
+ font: 500 13px/1 var(--atx-font-ui);
1642
+ white-space: nowrap;
1643
+ outline: none;
1644
+ cursor: pointer;
1645
+ transition: background 120ms, color 120ms;
1646
+ }
1647
+
1648
+ .atx-bar-btn-icon {
1649
+ width: 28px;
1650
+ padding: 0;
1651
+ }
1652
+
1653
+ .atx-bar-btn:focus-visible,
1654
+ .atx-menu-item:focus-visible {
1655
+ box-shadow: 0 0 0 2px ${hexToRgba(COLOR.ring, 0.7)};
1656
+ }
1657
+
1658
+ .atx-bar-btn:hover:not(:disabled) {
1659
+ background: ${BAR_CHIP_HOVER};
1660
+ }
1661
+
1662
+ .atx-menu-item {
1663
+ display: flex;
1664
+ align-items: center;
1665
+ gap: 8px;
1666
+ width: 100%;
1667
+ height: 32px;
1668
+ padding: 0 12px;
1669
+ border: none;
1670
+ border-radius: var(--atx-radius-md);
1671
+ background: transparent;
1672
+ color: var(--atx-foreground);
1673
+ font: 500 14px var(--atx-font-ui);
1674
+ text-align: left;
1675
+ outline: none;
1676
+ cursor: pointer;
1677
+ }
1678
+
1679
+ /* Hover shifts the surface; it does not tint it. A hue on hover competes with
1680
+ the one colour that is supposed to mean something. */
1681
+ .atx-menu-item:hover:not(:disabled) {
1682
+ background: var(--atx-accent);
1683
+ }
1684
+
1685
+ /* An item whose feature is currently on. Near-white with dark ink — the same
1686
+ emphasis the confirm button gets, and for the same reason. */
1687
+ .atx-bar-btn[data-active],
1688
+ .atx-menu-item[data-active] {
1689
+ background: var(--atx-primary);
1690
+ color: var(--atx-primary-fg);
1691
+ }
1692
+
1693
+ .atx-bar-btn[data-active]:hover:not(:disabled),
1694
+ .atx-menu-item[data-active]:hover:not(:disabled) {
1695
+ background: ${hexToRgba(COLOR.primary, 0.9)};
1696
+ }
1697
+
1698
+ .atx-bar-btn[data-off],
1699
+ .atx-menu-item[data-off] {
1700
+ cursor: default;
1701
+ }
1702
+
1703
+ .atx-bar-btn[data-hidden],
1704
+ .atx-menu-item[data-hidden] {
1705
+ display: none;
1706
+ }
1707
+
1708
+ .atx-menu {
1709
+ position: fixed;
1710
+ z-index: ${Z + 4};
1711
+ display: none;
1712
+ flex-direction: column;
1713
+ min-width: 220px;
1714
+ padding: 6px;
1715
+ border: 1px solid var(--atx-border);
1716
+ border-radius: var(--atx-radius-lg);
1717
+ background: ${hexToRgba(COLOR.glassRaised, 0.97)};
1718
+ backdrop-filter: blur(14px);
1719
+ color: var(--atx-foreground);
1720
+ font: 500 13px var(--atx-font-ui);
1721
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.42);
1722
+ cursor: auto;
1723
+ }
1724
+
1725
+ .atx-menu[data-on] {
1726
+ display: flex;
1727
+ }
1728
+
1729
+ .atx-menu-items {
1730
+ display: flex;
1731
+ flex-direction: column;
1732
+ }
1733
+
1734
+ .atx-menu-foot {
1735
+ display: flex;
1736
+ align-items: center;
1737
+ gap: 7px;
1738
+ margin: 6px 4px 0;
1739
+ padding-top: 8px;
1740
+ border-top: 1px solid var(--atx-border);
1741
+ color: var(--atx-faint-fg);
1742
+ font: 500 10.5px var(--atx-font-mono);
1743
+ }
1744
+
1745
+ .atx-menu-live {
1746
+ flex: 0 0 auto;
1747
+ width: 6px;
1748
+ height: 6px;
1749
+ border-radius: 50%;
1750
+ background: var(--atx-info);
1751
+ }
1752
+
1753
+ /* == Entry drawer fields ===================================================
1754
+ editors/fields.ts and editors/entry.ts. One control per FieldType, all
1755
+ wrapped in the same label/error/help chrome. Two states are attributes
1756
+ rather than inline writes: [data-locked] on a field whose option the
1757
+ project's config owns, and [data-on] on the error line, which is the only
1758
+ part of the stack that is conditionally present. */
1759
+
1760
+ /* No margin of its own: distance between fields belongs to the group that
1761
+ holds them (.atx-field-group), so a field is the same object wherever it is
1762
+ mounted and a lone one does not push a gap below itself. */
1763
+ .atx-field {
1764
+ display: flex;
1765
+ flex-direction: column;
1766
+ }
1767
+
1768
+ /* A field the project's own config owns. The dim lands on the control, not on
1769
+ the whole stack: the label and the help text are what explain *why* it is
1770
+ locked, and dimming the explanation along with the thing it explains is the
1771
+ one part a reader still needs at full strength. */
1772
+ .atx-field[data-locked] [data-input],
1773
+ .atx-field[data-locked] .atx-field-check {
1774
+ opacity: 0.5;
1775
+ }
1776
+
1777
+ /* Full opacity, not a dimmed foreground: a label is read, and dimming it was
1778
+ doing the job that a second ink tier does properly. */
1779
+ .atx-field-label {
1780
+ display: block;
1781
+ margin-bottom: 8px;
1782
+ color: var(--atx-foreground);
1783
+ font: 500 14px/1.4 var(--atx-font-ui);
1784
+ }
1785
+
1786
+ /* 14px, not a size down. shadcn's field description is the same size as the
1787
+ label and separated by colour alone, which is what keeps a form of mostly
1788
+ help text readable rather than a wall of small print. */
1789
+ .atx-field-help {
1790
+ margin-top: 6px;
1791
+ color: var(--atx-muted-fg);
1792
+ font: 400 14px/1.45 var(--atx-font-ui);
1793
+ }
1794
+
1795
+ .atx-field-error {
1796
+ display: none;
1797
+ margin-top: 6px;
1798
+ color: var(--atx-destructive);
1799
+ font: 400 12px/1.45 var(--atx-font-ui);
1800
+ }
1801
+
1802
+ .atx-field-error[data-on] {
1803
+ display: block;
1804
+ }
1805
+
1806
+ .atx-field-check {
1807
+ display: flex;
1808
+ align-items: center;
1809
+ gap: 8px;
1810
+ min-height: 36px;
1811
+ color: var(--atx-foreground);
1812
+ font: 400 14px var(--atx-font-ui);
1813
+ }
1814
+
1815
+ .atx-field-check-hint {
1816
+ opacity: 0.7;
1817
+ }
1818
+
1819
+ .atx-field-checkbox,
1820
+ .atx-field-select {
1821
+ cursor: pointer;
1822
+ }
1823
+
1824
+ /* field-sizing grows the box with the prose in it, which is what a excerpt or
1825
+ a description wants; min-height is the floor for browsers without it, so the
1826
+ fallback is a fixed textarea rather than a collapsed one. */
1827
+ .atx-field-textarea {
1828
+ min-height: 64px;
1829
+ field-sizing: content;
1830
+ max-height: 40vh;
1831
+ }
1832
+
1833
+ /* A shape the panel cannot edit: read-only, monospaced, and dimmed so it
1834
+ reads as a report of the file rather than as an input. */
1835
+ .atx-field-json {
1836
+ min-height: 48px;
1837
+ font: 400 12px/1.5 var(--atx-font-mono);
1838
+ opacity: 0.6;
1839
+ resize: vertical;
1840
+ }
1841
+
1842
+
1843
+
1844
+ /* == Settings drawer =======================================================
1845
+ editors/settings-panel.ts. The option controls themselves come from
1846
+ fields.ts; what is here is the drawer's own prose, the Unsplash key section
1847
+ and the three status lines. Four states are attributes: [data-off] on the
1848
+ key section while the photo source is switched off, [data-on] on the error
1849
+ and gitignore-warning lines, [data-hidden] on the clear-key button, and
1850
+ [data-tone] / [data-mono] on a status word. */
1851
+
1852
+ /* A stack of cards under the tab strip, spaced like the entry drawer's. */
1853
+ .atx-settings-pane {
1854
+ display: flex;
1855
+ flex-direction: column;
1856
+ gap: 16px;
1857
+ padding-top: 16px;
1858
+ }
1859
+
1860
+ .atx-settings-status,
1861
+ .atx-settings-key-status {
1862
+ display: flex;
1863
+ align-items: center;
1864
+ gap: 6px;
1865
+ font: 13px var(--atx-font-ui);
1866
+ }
1867
+
1868
+ .atx-settings-status {
1869
+ margin: 0;
1870
+ }
1871
+
1872
+ .atx-settings-key-status {
1873
+ margin: 0 0 10px;
1874
+ color: var(--atx-foreground);
1875
+ }
1876
+
1877
+ .atx-settings-error {
1878
+ display: none;
1879
+ margin: 10px 0 0;
1880
+ color: var(--atx-warning);
1881
+ font: 13px/1.5 var(--atx-font-ui);
1882
+ }
1883
+
1884
+ .atx-settings-warning {
1885
+ display: none;
1886
+ margin: 12px 0 0;
1887
+ color: var(--atx-warning);
1888
+ font: 12px/1.5 var(--atx-font-ui);
1889
+ }
1890
+
1891
+ .atx-settings-error[data-on],
1892
+ .atx-settings-warning[data-on] {
1893
+ display: block;
1894
+ }
1895
+
1896
+ /* The access key is a secret with its own endpoint, not an option, so it is a
1897
+ card of its own rather than a heading inside the options card. It dims
1898
+ whole while the photo source it belongs to is off. */
1899
+
1900
+ .atx-settings-key-section[data-off] {
1901
+ opacity: 0.55;
1902
+ }
1903
+
1904
+ /* Body size, muted -- a card description that happens to carry a link, so it
1905
+ matches .atx-card-desc rather than being a size down from it. */
1906
+ .atx-settings-blurb {
1907
+ margin: 0 0 12px;
1908
+ color: var(--atx-muted-fg);
1909
+ font: 400 14px/1.45 var(--atx-font-ui);
1910
+ }
1911
+
1912
+ .atx-settings-link {
1913
+ color: var(--atx-brand-text);
1914
+ }
1915
+
1916
+ .atx-settings-hint {
1917
+ margin: 8px 0 0;
1918
+ color: var(--atx-muted-fg);
1919
+ font: 12px/1.5 var(--atx-font-ui);
1920
+ }
1921
+
1922
+ .atx-settings-row {
1923
+ display: flex;
1924
+ gap: 6px;
1925
+ }
1926
+
1927
+ .atx-settings-key {
1928
+ flex: 1 1 auto;
1929
+ }
1930
+
1931
+ /* The reveal toggle keeps its own width beside the growing key field. */
1932
+ .atx-settings-row > .atx-btn-outline {
1933
+ flex: 0 0 auto;
1934
+ }
1935
+
1936
+ .atx-settings-key-actions {
1937
+ display: flex;
1938
+ }
1939
+
1940
+ /* A destructive button pushes itself to the far left of a footer; here it is
1941
+ the only thing in its own row, so that margin has nothing to do. */
1942
+ .atx-settings-key-actions > .atx-btn-destructive {
1943
+ margin-right: 0;
1944
+ }
1945
+
1946
+ .atx-settings-key-actions > .atx-btn-destructive[data-hidden] {
1947
+ display: none;
1948
+ }
1949
+
1950
+ .atx-settings-text {
1951
+ font: 13px var(--atx-font-ui);
1952
+ }
1953
+
1954
+ .atx-settings-text[data-tone='muted'] {
1955
+ color: var(--atx-muted-fg);
1956
+ }
1957
+
1958
+ /* successText, not success: the plain token is a *background* — a dark green
1959
+ that reaches 2.7:1 as ink on a panel, which is the mistake the two-token
1960
+ split exists to prevent. */
1961
+ .atx-settings-text[data-tone='ok'] {
1962
+ color: var(--atx-success-text);
1963
+ }
1964
+
1965
+ .atx-settings-text[data-tone='warn'] {
1966
+ color: var(--atx-warning);
1967
+ }
1968
+
1969
+ .atx-settings-text[data-mono] {
1970
+ font: 12px var(--atx-font-mono);
1971
+ }
1972
+
1973
+ /* Muted, not amber: an option the project set in its own config is a normal
1974
+ state, and the one warning colour is spent on the uncommitted-secret line. */
1975
+ .atx-settings-lock {
1976
+ display: flex;
1977
+ align-items: center;
1978
+ gap: 6px;
1979
+ margin: 8px 0 0;
1980
+ color: var(--atx-muted-fg);
1981
+ font: 12px/1.45 var(--atx-font-ui);
1982
+ }
1983
+
1984
+ /* == Collections drawer ====================================================
1985
+ editors/collections-panel.ts. A field row spans two stores, and the drawer's
1986
+ job is to keep saying which is which, so most of what is here is the chrome
1987
+ that does the saying: the legend, the two captioned groups, the badges.
1988
+ States are attributes: [data-removed] on a card whose removal is queued,
1989
+ [data-off] on the schema half of a field the project keeps read-only,
1990
+ [data-hidden] on the Options row, which only a select field has,
1991
+ [data-on] on the error line, and [data-tone] on a badge or a note. */
1992
+
1993
+ .atx-collections {
1994
+ padding-top: 12px;
1995
+ }
1996
+
1997
+ /* The entry rows are whole-width buttons that read as rows in a list. The
1998
+ collection rows come from group.ts::item and only add the pointer -- these
1999
+ carry the same box as one, so both lists in the drawer are the same object. */
2000
+ .atx-collections-item {
2001
+ display: block;
2002
+ width: 100%;
2003
+ margin-bottom: 8px;
2004
+ padding: 10px 12px;
2005
+ border: 1px solid var(--atx-border);
2006
+ border-radius: var(--atx-radius-lg);
2007
+ background: var(--atx-card);
2008
+ color: var(--atx-foreground);
2009
+ text-align: left;
2010
+ cursor: pointer;
2011
+ }
2012
+
2013
+ .atx-collections-row {
2014
+ cursor: pointer;
2015
+ }
2016
+
2017
+ /* Hover moves the surface, the way a menu item and a tree row do. The chevron
2018
+ is the affordance at rest; the fill is the confirmation under the pointer. */
2019
+ .atx-collections-row:hover,
2020
+ .atx-collections-row:focus-visible {
2021
+ background: var(--atx-accent);
2022
+ outline: none;
2023
+ }
2024
+
2025
+ .atx-collections-row:focus-visible {
2026
+ box-shadow: 0 0 0 2px var(--atx-ring);
2027
+ }
2028
+
2029
+ .atx-collections-item-title {
2030
+ display: flex;
2031
+ align-items: center;
2032
+ gap: 8px;
2033
+ }
2034
+
2035
+ /* The row's title comes from group.ts::item, which already sets the type; it
2036
+ only needs room for the badges that sit beside the name. */
2037
+ .atx-collections-row .atx-item-title {
2038
+ display: flex;
2039
+ align-items: center;
2040
+ gap: 8px;
2041
+ overflow: visible;
2042
+ }
2043
+
2044
+ .atx-collections-item-title {
2045
+ font: 500 14px var(--atx-font-ui);
2046
+ }
2047
+
2048
+ .atx-collections-item-meta,
2049
+ .atx-collections-meta {
2050
+ color: var(--atx-muted-fg);
2051
+ font: 12px var(--atx-font-mono);
2052
+ }
2053
+
2054
+ /* Monospaced, because it is a path and two counts -- data about the row, not
2055
+ prose. Same size as any other item description.
2056
+
2057
+ It wraps rather than ellipsing, unlike every other item description: the
2058
+ switch and its label take real width out of this row, and a truncated
2059
+ directory tells you less than a wrapped one. The separators are spaces, so a
2060
+ wrap lands between two facts rather than inside a path. */
2061
+ .atx-collections-row .atx-item-desc {
2062
+ overflow: visible;
2063
+ white-space: normal;
2064
+ text-overflow: clip;
2065
+ font-family: var(--atx-font-mono);
2066
+ }
2067
+
2068
+ .atx-collections-item-meta {
2069
+ margin-top: 4px;
2070
+ }
2071
+
2072
+ /* The line under a collection's name: which directory, how many entries, what
2073
+ kind of schema. 12px is the item-description size, not a size below it -- it
2074
+ is read, and shrinking it was the reason the header felt crowded. */
2075
+ .atx-collections-meta {
2076
+ margin-top: 4px;
2077
+ margin-bottom: 16px;
2078
+ font-size: 12px;
2079
+ }
2080
+
2081
+ /* The one button on the collections list, under the rows. */
2082
+ .atx-collections-list > .atx-btn-outline {
2083
+ margin-top: 8px;
2084
+ }
2085
+
2086
+ .atx-collections-fieldspane,
2087
+ .atx-collections-items {
2088
+ padding-top: 16px;
2089
+ }
2090
+
2091
+ .atx-collections-head,
2092
+ .atx-collections-itembar {
2093
+ display: flex;
2094
+ align-items: center;
2095
+ gap: 8px;
2096
+ }
2097
+
2098
+ .atx-collections-head {
2099
+ margin-bottom: 8px;
2100
+ }
2101
+
2102
+ .atx-collections-head-create {
2103
+ margin-bottom: 16px;
2104
+ }
2105
+
2106
+ .atx-collections-itembar {
2107
+ margin-bottom: 16px;
2108
+ }
2109
+
2110
+ .atx-collections-title {
2111
+ color: var(--atx-foreground);
2112
+ font: 500 14px var(--atx-font-ui);
2113
+ }
2114
+
2115
+ .atx-collections-spacer {
2116
+ flex: 1 1 auto;
2117
+ }
2118
+
2119
+ .atx-collections-itemcount {
2120
+ flex: 1 1 auto;
2121
+ color: var(--atx-muted-fg);
2122
+ font: 14px var(--atx-font-ui);
2123
+ }
2124
+
2125
+ /* A column with a gap rather than a margin on each card: the gap belongs to
2126
+ the list, so a card is the same object wherever it is mounted and the last
2127
+ one does not push a space below itself. 12px is the item-group rung. */
2128
+ .atx-collections-fields,
2129
+ .atx-collections-newfields {
2130
+ display: flex;
2131
+ flex-direction: column;
2132
+ gap: 12px;
2133
+ margin-top: 16px;
2134
+ }
2135
+
2136
+ .atx-collections-error {
2137
+ display: none;
2138
+ margin: 16px 0 0;
2139
+ color: var(--atx-warning);
2140
+ font: 13px/1.5 var(--atx-font-ui);
2141
+ }
2142
+
2143
+ .atx-collections-error[data-on] {
2144
+ display: block;
2145
+ }
2146
+
2147
+ /* The footer band's action slot, filled by whichever view the pane is showing.
2148
+ display:contents so an empty slot takes no space in the band and a filled one
2149
+ lays its button out as if the slot were not there -- the list view leaves it
2150
+ empty, and a 0-width flex item would still collect the band's gap. */
2151
+ .atx-collections-primary {
2152
+ display: contents;
2153
+ }
2154
+
2155
+ /* One field: its name and remove control, then the two stores side by side.
2156
+ 16px all round and on the item rung for radius -- this is a card in a list
2157
+ of cards, and the 10/12 it used to carry made a form of nine of them read as
2158
+ one dense block rather than nine things. */
2159
+ .atx-collections-field {
2160
+ padding: 16px;
2161
+ border: 1px solid var(--atx-border);
2162
+ border-radius: var(--atx-radius-lg);
2163
+ background: var(--atx-card);
2164
+ }
2165
+
2166
+ /* Queued for removal, not removed: the card dims and the button offers the
2167
+ undo, and nothing is written until Save. */
2168
+ .atx-collections-field[data-removed] {
2169
+ opacity: 0.45;
2170
+ }
2171
+
2172
+ .atx-collections-field-head {
2173
+ display: flex;
2174
+ align-items: center;
2175
+ gap: 8px;
2176
+ margin-bottom: 14px;
2177
+ }
2178
+
2179
+ .atx-collections-field-name {
2180
+ color: var(--atx-foreground);
2181
+ font: 600 12px var(--atx-font-mono);
2182
+ }
2183
+
2184
+ /* The zod expression this field currently compiles to, verbatim. Set off by
2185
+ the card's own rule, so it reads as a footnote about the field rather than
2186
+ as one more line of it. */
2187
+ .atx-collections-expr {
2188
+ display: block;
2189
+ margin-top: 16px;
2190
+ padding-top: 12px;
2191
+ border-top: 1px solid var(--atx-border);
2192
+ color: var(--atx-muted-fg);
2193
+ font: 12px var(--atx-font-mono);
2194
+ word-break: break-all;
2195
+ }
2196
+
2197
+ .atx-collections-addfield {
2198
+ margin-top: 16px;
2199
+ padding: 16px;
2200
+ border: 1px dashed var(--atx-border);
2201
+ border-radius: var(--atx-radius-lg);
2202
+ }
2203
+
2204
+ .atx-collections-addfield > .atx-btn-outline {
2205
+ margin-top: 12px;
2206
+ }
2207
+
2208
+ /* The two-store legend — the one piece of chrome that explains the drawer. */
2209
+ .atx-collections-legend {
2210
+ padding: 16px;
2211
+ border: 1px solid var(--atx-border);
2212
+ border-radius: var(--atx-radius-xl);
2213
+ background: var(--atx-background);
2214
+ color: var(--atx-muted-fg);
2215
+ font: 13px/1.6 var(--atx-font-ui);
2216
+ }
2217
+
2218
+ /* The colour is repeated from the wrapper rather than inherited: a host page's
2219
+ own paragraph colour rule outranks an inherited value, and silently
2220
+ repainted this legend in the page's body colour. */
2221
+ .atx-collections-legend-line {
2222
+ margin: 0;
2223
+ color: var(--atx-muted-fg);
2224
+ }
2225
+
2226
+ .atx-collections-legend-word {
2227
+ color: var(--atx-foreground);
2228
+ }
2229
+
2230
+ /* The field's two stores, side by side: they describe the *same* field, and
2231
+ the card's whole job is letting you read one against the other. auto-fit
2232
+ rather than a fixed pair -- a drawer narrow enough that a control would be
2233
+ squeezed off its label stacks them again instead. */
2234
+ .atx-collections-stores {
2235
+ display: grid;
2236
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
2237
+ align-items: start;
2238
+ gap: 16px 20px;
2239
+ }
2240
+
2241
+ .atx-collections-group {
2242
+ min-width: 0;
2243
+ padding-left: 14px;
2244
+ border-left: 2px solid var(--atx-border);
2245
+ }
2246
+
2247
+ .atx-collections-group[data-off] {
2248
+ opacity: 0.6;
2249
+ }
2250
+
2251
+ .atx-collections-caption {
2252
+ margin-bottom: 12px;
2253
+ color: var(--atx-muted-fg);
2254
+ font: 500 12px var(--atx-font-ui);
2255
+ letter-spacing: 0.06em;
2256
+ text-transform: uppercase;
2257
+ }
2258
+
2259
+ /* Muted sits on the label below, not here: colour set on the row is inherited
2260
+ by whatever the row holds, which quietly greys the value as well as the word
2261
+ naming it. */
2262
+ .atx-collections-control {
2263
+ display: flex;
2264
+ align-items: center;
2265
+ gap: 12px;
2266
+ margin: 0 0 12px;
2267
+ color: var(--atx-foreground);
2268
+ font: 14px var(--atx-font-ui);
2269
+ }
2270
+
2271
+ /* The last control in a store carries no gap of its own -- the card's padding
2272
+ is the space under it. */
2273
+ .atx-collections-control:last-child {
2274
+ margin-bottom: 0;
2275
+ }
2276
+
2277
+ /* Options belong to a select field and to nothing else. */
2278
+ .atx-collections-control[data-hidden] {
2279
+ display: none;
2280
+ }
2281
+
2282
+ .atx-collections-control-label {
2283
+ flex: 0 0 78px;
2284
+ color: var(--atx-muted-fg);
2285
+ }
2286
+
2287
+ /* min-width:0 so a control shrinks with its column: side by side, a store
2288
+ is half the card wide, and an input's intrinsic width would otherwise push
2289
+ the row past it. */
2290
+ .atx-collections-input,
2291
+ .atx-collections-select {
2292
+ min-width: 0;
2293
+ flex: 1 1 auto;
2294
+ }
2295
+
2296
+ .atx-collections-checkbox {
2297
+ display: flex;
2298
+ flex: 1 1 auto;
2299
+ align-items: center;
2300
+ gap: 6px;
2301
+ }
2302
+
2303
+ .atx-collections-check {
2304
+ margin: 0;
2305
+ accent-color: var(--atx-primary);
2306
+ }
2307
+
2308
+ .atx-collections-check-hint {
2309
+ color: var(--atx-muted-fg);
2310
+ font: 14px var(--atx-font-ui);
2311
+ }
2312
+
2313
+ /* The Content editor switch rides in a collection row's action slot, beside the
2314
+ chevron. Its label is a fixed two words, so nothing shifts as it flips. */
2315
+ .atx-collections-pageedit {
2316
+ flex: 0 0 auto;
2317
+ }
2318
+
2319
+ /* The detail route a collection was matched to. Its own line under the
2320
+ description, one step quieter, because it answers a different question: not
2321
+ what this collection is, but where the switch beside it takes effect. */
2322
+ .atx-collections-route {
2323
+ display: flex;
2324
+ align-items: center;
2325
+ gap: 4px;
2326
+ margin-top: 3px;
2327
+ color: var(--atx-faint-fg);
2328
+ font: 11px/1.4 var(--atx-font-mono);
2329
+ }
2330
+
2331
+ /* The meta-tag snippet a collection with no detected route offers. The note is
2332
+ a row of icon + text until it carries one; then the block and its copy button
2333
+ each take a line of their own. */
2334
+ .atx-collections-note-snippet {
2335
+ flex-wrap: wrap;
2336
+ }
2337
+
2338
+ .atx-collections-snippet {
2339
+ flex: 1 0 100%;
2340
+ margin: 8px 0 6px;
2341
+ padding: 8px 10px;
2342
+ overflow-x: auto;
2343
+ border-radius: var(--atx-radius-md);
2344
+ background: var(--atx-background);
2345
+ color: var(--atx-foreground);
2346
+ font: 11px/1.55 var(--atx-font-mono);
2347
+ white-space: pre;
2348
+ }
2349
+
2350
+ /* A field typed into the add form but not yet written. Outlined in the brand
2351
+ colour because it is the only thing on screen that is not yet in the file. */
2352
+ .atx-collections-new {
2353
+ display: flex;
2354
+ align-items: center;
2355
+ gap: 8px;
2356
+ padding: 10px 12px;
2357
+ border: 1px solid var(--atx-brand);
2358
+ border-radius: var(--atx-radius-lg);
2359
+ background: var(--atx-card);
2360
+ }
2361
+
2362
+ .atx-collections-new-name {
2363
+ color: var(--atx-foreground);
2364
+ font: 600 12px var(--atx-font-mono);
2365
+ }
2366
+
2367
+ .atx-collections-new-meta {
2368
+ flex: 1 1 auto;
2369
+ color: var(--atx-muted-fg);
2370
+ font: 12px var(--atx-font-ui);
2371
+ }
2372
+
2373
+ /* The way back out of a collection. It wears the corner-action shape from the
2374
+ button system and adds only the one thing that shape cannot know: the
2375
+ chevron points the other way, since it is the forward one turned around.
2376
+
2377
+ Neutral, not brand: brandText means *this is on your page and editable*,
2378
+ and a back button is the tool talking about its own navigation. */
2379
+ .atx-collections-back > .atx-ico:first-child {
2380
+ transform: rotate(180deg);
2381
+ }
2382
+
2383
+ .atx-collections-blurb {
2384
+ margin: 0 0 12px;
2385
+ color: var(--atx-muted-fg);
2386
+ font: 13px/1.5 var(--atx-font-ui);
2387
+ }
2388
+
2389
+ .atx-collections-note {
2390
+ display: flex;
2391
+ align-items: flex-start;
2392
+ gap: 5px;
2393
+ margin: 0 0 10px;
2394
+ font: 12px/1.5 var(--atx-font-ui);
2395
+ }
2396
+
2397
+ .atx-collections-note[data-tone='warn'] {
2398
+ color: var(--atx-warning);
2399
+ }
2400
+
2401
+ .atx-collections-note[data-tone='muted'] {
2402
+ color: var(--atx-muted-fg);
2403
+ }
2404
+
2405
+ /* == Image panel and asset picker ==========================================
2406
+ editors/image.ts (the in-page <img> panel) and editors/asset-picker.ts (the
2407
+ entry drawer's image field). Both show a preview over a checkerboard, so a
2408
+ transparent PNG reads as transparent rather than as a hole. An <img> that
2409
+ fails to load takes [data-hidden] rather than showing a broken-image glyph;
2410
+ a retry that finally succeeds removes it again — see ui.ts::setFreshSrc. */
2411
+
2412
+ .atx-image-preview {
2413
+ width: 100%;
2414
+ height: 180px;
2415
+ max-height: 180px;
2416
+ margin-bottom: 10px;
2417
+ overflow: hidden;
2418
+ border: 1px solid var(--atx-border);
2419
+ border-radius: var(--atx-radius-lg);
2420
+ background: ${CHECKER(14)};
2421
+ }
2422
+
2423
+ /* No display of its own, deliberately: it inherits the inline default, which
2424
+ the 180px overflow-hidden box above clips to the same pixels a block would
2425
+ occupy. Plan 2 can settle it; a conversion may not. */
2426
+ .atx-image-preview-img {
2427
+ width: 100%;
2428
+ height: 100%;
2429
+ object-fit: contain;
2430
+ }
2431
+
2432
+ .atx-image-meta {
2433
+ margin: 0 0 12px;
2434
+ overflow: hidden;
2435
+ color: var(--atx-muted-fg);
2436
+ font: 11px var(--atx-font-mono);
2437
+ white-space: nowrap;
2438
+ text-overflow: ellipsis;
2439
+ }
2440
+
2441
+ .atx-note {
2442
+ margin: 0 0 12px;
2443
+ color: var(--atx-warning);
2444
+ font: 400 13px/1.5 var(--atx-font-ui);
2445
+ }
2446
+
2447
+ .atx-alt-label {
2448
+ display: block;
2449
+ margin-bottom: 6px;
2450
+ color: var(--atx-foreground);
2451
+ font: 500 14px/1 var(--atx-font-ui);
2452
+ }
2453
+
2454
+ /* Hand-built rather than via inputEl, so it carries the control baseline
2455
+ itself. Keep it in step with [data-input]. */
2456
+ .atx-alt-input {
2457
+ width: 100%;
2458
+ min-height: 32px;
2459
+ box-sizing: border-box;
2460
+ margin-bottom: 16px;
2461
+ padding: 4px 10px;
2462
+ border: 1px solid transparent;
2463
+ border-radius: var(--atx-radius-lg);
2464
+ background: var(--atx-input-bg);
2465
+ color: var(--atx-foreground);
2466
+ font: 400 14px/1.45 var(--atx-font-ui);
2467
+ outline: none;
2468
+ transition: background 200ms, border-color 200ms, box-shadow 200ms;
2469
+ }
2470
+
2471
+ .atx-alt-input:focus-visible {
2472
+ border-color: var(--atx-ring);
2473
+ box-shadow: 0 0 0 3px ${hexToRgba(COLOR.ring, 0.3)};
2474
+ }
2475
+
2476
+ /* Alt text that comes from an expression: readable, but not yours to type in. */
2477
+ .atx-alt-input[data-off] {
2478
+ opacity: 0.5;
2479
+ }
2480
+
2481
+ /* The six most recent assets. Mirrors RECENTS in editors/image.ts. */
2482
+ .atx-image-recents {
2483
+ display: grid;
2484
+ grid-template-columns: repeat(6, 1fr);
2485
+ gap: 6px;
2486
+ }
2487
+
2488
+ .atx-image-recents-label {
2489
+ display: flex;
2490
+ align-items: baseline;
2491
+ gap: 8px;
2492
+ margin: 0 0 8px;
2493
+ color: var(--atx-foreground);
2494
+ font: 500 14px/1 var(--atx-font-ui);
2495
+ }
2496
+
2497
+ /* The strip is a convenience; if its listing fails it goes away quietly and
2498
+ the modal's Browse all still works. */
2499
+ .atx-image-recents-label[data-hidden] {
2500
+ display: none;
2501
+ }
2502
+
2503
+ /* Wears the corner-action shape; the only thing left here is where it sits. */
2504
+ .atx-image-browse-all {
2505
+ margin-left: auto;
2506
+ }
2507
+
2508
+ .atx-image-recent {
2509
+ width: 100%;
2510
+ padding: 0;
2511
+ aspect-ratio: 4 / 3;
2512
+ overflow: hidden;
2513
+ border: 1px solid var(--atx-border);
2514
+ border-radius: var(--atx-radius-lg);
2515
+ outline: none;
2516
+ background: ${CHECKER(10)};
2517
+ cursor: pointer;
2518
+ }
2519
+
2520
+ /* The one already on the page. */
2521
+ .atx-image-recent[data-current] {
2522
+ border-color: var(--atx-primary);
2523
+ outline: 1px solid var(--atx-primary);
2524
+ }
2525
+
2526
+ .atx-image-recent-thumb {
2527
+ display: block;
2528
+ width: 100%;
2529
+ height: 100%;
2530
+ object-fit: cover;
2531
+ }
2532
+
2533
+ .atx-image-preview-img[data-hidden],
2534
+ .atx-image-recent-thumb[data-hidden],
2535
+ .atx-image-field-thumb[data-hidden],
2536
+ .atx-image-field-empty[data-hidden],
2537
+ .atx-media-thumb[data-hidden],
2538
+ .atx-media-rail-img[data-hidden] {
2539
+ display: none;
2540
+ }
2541
+
2542
+ .atx-image-field {
2543
+ display: grid;
2544
+ gap: 8px;
2545
+ }
2546
+
2547
+ .atx-image-field-preview {
2548
+ display: flex;
2549
+ align-items: center;
2550
+ justify-content: center;
2551
+ width: 240px;
2552
+ height: 160px;
2553
+ padding: 0;
2554
+ overflow: hidden;
2555
+ border: 1px solid var(--atx-border);
2556
+ border-radius: var(--atx-radius-lg);
2557
+ background: ${CHECKER(16)};
2558
+ cursor: pointer;
2559
+ }
2560
+
2561
+ .atx-image-field-thumb {
2562
+ width: 100%;
2563
+ height: 100%;
2564
+ object-fit: cover;
2565
+ }
2566
+
2567
+ .atx-image-field-empty {
2568
+ color: var(--atx-muted-fg);
2569
+ font: 400 13px var(--atx-font-ui);
2570
+ pointer-events: none;
2571
+ }
2572
+
2573
+ .atx-image-field-row {
2574
+ display: flex;
2575
+ align-items: center;
2576
+ gap: 8px;
2577
+ }
2578
+
2579
+ .atx-image-field-path {
2580
+ flex: 1 1 auto;
2581
+ min-width: 0;
2582
+ font: 12px var(--atx-font-mono);
2583
+ }
2584
+
2585
+ /* Sits beside the path field in a flex row, so it takes the field's height
2586
+ and shape rather than a button's own. */
2587
+ /* Wears the shared outline button; the flex basis is the only thing left that
2588
+ belongs to this row rather than to the button system. */
2589
+ .atx-image-field-browse {
2590
+ flex: 0 0 auto;
2591
+ }
2592
+
2593
+ .atx-image-field-hint {
2594
+ color: var(--atx-muted-fg);
2595
+ font: 400 12px var(--atx-font-mono);
2596
+ }
2597
+
2598
+ /* == Media grid ============================================================
2599
+ editors/media-grid.ts. One tile shape for both panes; the caption sits
2600
+ outside the pick button so a credit link is clickable. [data-selected] is
2601
+ the ring and the tick together, and [aria-busy] — which the tile already
2602
+ carries for assistive tech — is also what dims it during an import. */
2603
+
2604
+ .atx-media-pane {
2605
+ flex: 1 1 auto;
2606
+ min-height: 0;
2607
+ padding: 2px;
2608
+ overflow-y: auto;
2609
+ }
2610
+
2611
+ /* 132px is the narrowest a tile can be and still read as a picture. */
2612
+ .atx-media-grid {
2613
+ display: grid;
2614
+ grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
2615
+ align-content: start;
2616
+ gap: 12px;
2617
+ }
2618
+
2619
+ .atx-media-more {
2620
+ display: flex;
2621
+ justify-content: center;
2622
+ padding: 14px 0 4px;
2623
+ }
2624
+
2625
+ .atx-media-tile {
2626
+ position: relative;
2627
+ min-width: 0;
2628
+ }
2629
+
2630
+ .atx-media-pick {
2631
+ position: relative;
2632
+ display: block;
2633
+ width: 100%;
2634
+ padding: 0;
2635
+ aspect-ratio: 4 / 3;
2636
+ overflow: hidden;
2637
+ border: 1px solid var(--atx-border);
2638
+ border-radius: var(--atx-radius-lg);
2639
+ background: ${CHECKER(12)};
2640
+ outline: 2px solid transparent;
2641
+ outline-offset: 2px;
2642
+ transition: outline-color 100ms;
2643
+ cursor: pointer;
2644
+ }
2645
+
2646
+ .atx-media-pick[data-selected] {
2647
+ outline-color: var(--atx-primary);
2648
+ }
2649
+
2650
+ .atx-media-pick[aria-busy='true'] {
2651
+ opacity: 0.45;
2652
+ pointer-events: none;
2653
+ cursor: progress;
2654
+ }
2655
+
2656
+ /* A tile that cannot be picked *here* -- an image() asset in a web-path picker
2657
+ or the reverse. It stays on screen and says why: the dimming is the shared
2658
+ :disabled rule above, and this is only the band that carries the reason.
2659
+ Text over the thumbnail, so it needs its own scrim rather than a token. */
2660
+ .atx-media-reason {
2661
+ position: absolute;
2662
+ right: 0;
2663
+ bottom: 0;
2664
+ left: 0;
2665
+ padding: 3px 6px;
2666
+ background: rgba(0, 0, 0, 0.72);
2667
+ color: var(--atx-foreground);
2668
+ font: 500 11px/1.4 var(--atx-font-ui);
2669
+ text-align: center;
2670
+ pointer-events: none;
2671
+ }
2672
+
2673
+ /* The caption belongs to the same tile, so it dims with it -- the :disabled
2674
+ rule above only reaches the button, and a full-strength filename under a
2675
+ greyed-out thumbnail reads as a rendering fault. */
2676
+ .atx-media-tile[data-off] .atx-media-cap {
2677
+ opacity: 0.5;
2678
+ }
2679
+
2680
+ .atx-media-thumb {
2681
+ display: block;
2682
+ width: 100%;
2683
+ height: 100%;
2684
+ object-fit: cover;
2685
+ }
2686
+
2687
+ /* Shown in place of an image that could not load — blocked by a CSP, offline,
2688
+ or deleted. The tile stays usable: its caption reads and it still picks. */
2689
+ .atx-media-fallback {
2690
+ display: none;
2691
+ align-items: center;
2692
+ justify-content: center;
2693
+ width: 100%;
2694
+ height: 100%;
2695
+ color: var(--atx-faint-fg);
2696
+ }
2697
+
2698
+ .atx-media-fallback[data-on] {
2699
+ display: flex;
2700
+ }
2701
+
2702
+ .atx-media-check {
2703
+ position: absolute;
2704
+ top: 6px;
2705
+ right: 6px;
2706
+ display: none;
2707
+ align-items: center;
2708
+ justify-content: center;
2709
+ width: 20px;
2710
+ height: 20px;
2711
+ border-radius: 50%;
2712
+ background: var(--atx-primary);
2713
+ color: var(--atx-primary-fg);
2714
+ pointer-events: none;
2715
+ }
2716
+
2717
+ .atx-media-pick[data-selected] .atx-media-check {
2718
+ display: flex;
2719
+ }
2720
+
2721
+ .atx-media-current {
2722
+ position: absolute;
2723
+ top: 6px;
2724
+ left: 6px;
2725
+ padding: 2px 6px;
2726
+ border-radius: var(--atx-radius-sm);
2727
+ background: rgba(0, 0, 0, 0.7);
2728
+ color: var(--atx-foreground);
2729
+ font: 500 11px var(--atx-font-ui);
2730
+ pointer-events: none;
2731
+ }
2732
+
2733
+ .atx-media-skeleton-thumb {
2734
+ width: 100%;
2735
+ aspect-ratio: 4 / 3;
2736
+ border: 1px solid var(--atx-border);
2737
+ border-radius: var(--atx-radius-lg);
2738
+ background: var(--atx-elevated);
2739
+ }
2740
+
2741
+ .atx-media-skeleton-cap {
2742
+ height: 10px;
2743
+ margin: 6px 0 0;
2744
+ border-radius: var(--atx-radius-sm);
2745
+ background: var(--atx-elevated);
2746
+ }
2747
+
2748
+ /* A message is a full-width row inside the grid, never laid out as a tile. */
2749
+ .atx-media-status {
2750
+ grid-column: 1 / -1;
2751
+ padding: 28px 12px;
2752
+ color: var(--atx-muted-fg);
2753
+ font: 14px/1.6 var(--atx-font-ui);
2754
+ text-align: center;
2755
+ }
2756
+
2757
+ /* Three buttons that were each a hand-copied outline variant -- same border,
2758
+ radius, height, ink, hover and focus ring, written out three times and free
2759
+ to drift. They wear .atx-btn-outline now; what is left here is only what is
2760
+ theirs: where each one sits. */
2761
+ .atx-btn-retry {
2762
+ margin: 12px auto 0;
2763
+ }
2764
+
2765
+ .atx-media-cap {
2766
+ margin: 6px 2px 0;
2767
+ overflow: hidden;
2768
+ color: var(--atx-muted-fg);
2769
+ font: 11px/1.4 var(--atx-font-mono);
2770
+ white-space: nowrap;
2771
+ text-overflow: ellipsis;
2772
+ }
2773
+
2774
+ /* A credit is prose, not a filename. It is permanently visible rather than
2775
+ revealed on hover: that is what the API guidelines ask for. */
2776
+ .atx-media-cap[data-credit] {
2777
+ font: 12px/1.4 var(--atx-font-ui);
2778
+ }
2779
+
2780
+ .atx-unsplash-credit {
2781
+ color: var(--atx-brand-text);
2782
+ }
2783
+
2784
+ /* == Media modal ===========================================================
2785
+ editors/media-modal.ts. The sized panel that holds both panes, the detail
2786
+ rail and the drop target. [data-on] opens the drag overlay and reveals the
2787
+ scope toggle, which only a scoped listing has anything to say with. */
2788
+
2789
+ /* Matched at the sized panel's own specificity: .atx-panel[data-sized]
2790
+ .atx-panel-body makes the body the scrolling region, and this body is the
2791
+ one that must not scroll — the grid inside it does. */
2792
+ .atx-media-body {
2793
+ display: flex;
2794
+ flex-direction: column;
2795
+ gap: 10px;
2796
+ }
2797
+
2798
+ .atx-panel[data-sized] .atx-media-body {
2799
+ overflow: hidden;
2800
+ }
2801
+
2802
+ .atx-media-tabhost {
2803
+ flex: 0 0 auto;
2804
+ }
2805
+
2806
+ .atx-media-upload {
2807
+ margin-left: auto;
2808
+ }
2809
+
2810
+ .atx-media-file {
2811
+ display: none;
2812
+ }
2813
+
2814
+ .atx-media-content {
2815
+ display: flex;
2816
+ flex: 1 1 auto;
2817
+ min-height: 0;
2818
+ }
2819
+
2820
+ .atx-media-panes {
2821
+ display: flex;
2822
+ flex: 1 1 auto;
2823
+ flex-direction: column;
2824
+ gap: 10px;
2825
+ min-width: 0;
2826
+ min-height: 0;
2827
+ }
2828
+
2829
+ .atx-media-rail {
2830
+ flex: 0 0 280px;
2831
+ width: 280px;
2832
+ margin-left: 14px;
2833
+ padding-left: 14px;
2834
+ border-left: 1px solid var(--atx-border);
2835
+ overflow-y: auto;
2836
+ }
2837
+
2838
+ .atx-media-drop {
2839
+ flex: 0 0 auto;
2840
+ color: var(--atx-muted-fg);
2841
+ font: 11px var(--atx-font-mono);
2842
+ text-align: center;
2843
+ }
2844
+
2845
+ .atx-media-foot-status {
2846
+ grid-column: auto;
2847
+ margin-right: auto;
2848
+ padding: 0;
2849
+ font: 13px var(--atx-font-ui);
2850
+ text-align: left;
2851
+ }
2852
+
2853
+ /* A dedicated dashed box would eat grid height, so the modal itself is the
2854
+ drop target and this tints it while a file is over it. */
2855
+ .atx-media-dropzone {
2856
+ position: absolute;
2857
+ inset: 0;
2858
+ z-index: 2;
2859
+ display: none;
2860
+ border: 2px dashed var(--atx-primary);
2861
+ border-radius: var(--atx-radius-md);
2862
+ background: ${hexToRgba(COLOR.primary, 0.18)};
2863
+ pointer-events: none;
2864
+ }
2865
+
2866
+ .atx-media-dropzone[data-on] {
2867
+ display: block;
2868
+ }
2869
+
2870
+ .atx-media-toolbar {
2871
+ display: flex;
2872
+ align-items: center;
2873
+ gap: 6px;
2874
+ }
2875
+
2876
+ .atx-asset-filter {
2877
+ flex: 1 1 auto;
2878
+ min-width: 0;
2879
+ font: 12px var(--atx-font-mono);
2880
+ }
2881
+
2882
+ /* Hidden until a listing has folders worth scoping to, so its display is a
2883
+ state rather than the shared inline-flex above. */
2884
+ .atx-asset-scope {
2885
+ display: none;
2886
+ flex: 0 0 auto;
2887
+ }
2888
+
2889
+ .atx-asset-scope[data-on] {
2890
+ display: inline-flex;
2891
+ }
2892
+
2893
+ .atx-media-sort,
2894
+ .atx-unsplash-orient,
2895
+ .atx-unsplash-width {
2896
+ flex: 0 0 auto;
2897
+ width: auto;
2898
+ font: 13px var(--atx-font-ui);
2899
+ }
2900
+
2901
+ /* --- the detail rail --- */
2902
+
2903
+ .atx-media-rail-empty {
2904
+ margin: 0;
2905
+ color: var(--atx-muted-fg);
2906
+ font: 13px/1.6 var(--atx-font-ui);
2907
+ }
2908
+
2909
+ .atx-media-rail-preview {
2910
+ width: 100%;
2911
+ aspect-ratio: 4 / 3;
2912
+ margin-bottom: 10px;
2913
+ overflow: hidden;
2914
+ border: 1px solid var(--atx-border);
2915
+ border-radius: var(--atx-radius-lg);
2916
+ background: ${CHECKER(12)};
2917
+ }
2918
+
2919
+ .atx-media-rail-img {
2920
+ display: block;
2921
+ width: 100%;
2922
+ height: 100%;
2923
+ object-fit: contain;
2924
+ }
2925
+
2926
+ .atx-media-rail-title {
2927
+ margin: 0 0 8px;
2928
+ overflow: hidden;
2929
+ color: var(--atx-foreground);
2930
+ font: 500 14px var(--atx-font-ui);
2931
+ text-overflow: ellipsis;
2932
+ }
2933
+
2934
+ .atx-media-rail-line {
2935
+ margin: 0 0 6px;
2936
+ }
2937
+
2938
+ .atx-media-rail-key {
2939
+ display: block;
2940
+ color: var(--atx-muted-fg);
2941
+ font: 500 11px var(--atx-font-ui);
2942
+ letter-spacing: 0.04em;
2943
+ text-transform: uppercase;
2944
+ }
2945
+
2946
+ .atx-media-rail-value {
2947
+ display: block;
2948
+ color: var(--atx-foreground);
2949
+ font: 11px/1.5 var(--atx-font-mono);
2950
+ word-break: break-all;
2951
+ }
2952
+
2953
+ .atx-media-rail-link {
2954
+ color: var(--atx-brand-text);
2955
+ font: 13px/1.5 var(--atx-font-ui);
2956
+ word-break: normal;
2957
+ }
2958
+
2959
+ /* == Unsplash pane =========================================================
2960
+ editors/unsplash-pane.ts. The search box is a div wearing the control
2961
+ baseline, because the magnifier and the field share one bordered box. */
2962
+
2963
+ .atx-unsplash-search {
2964
+ display: flex;
2965
+ flex: 1 1 auto;
2966
+ align-items: center;
2967
+ gap: 6px;
2968
+ min-width: 0;
2969
+ padding: 4px 8px;
2970
+ }
2971
+
2972
+ .atx-unsplash-search > .atx-ico {
2973
+ opacity: 0.6;
2974
+ }
2975
+
2976
+ .atx-unsplash-input {
2977
+ flex: 1 1 auto;
2978
+ min-width: 0;
2979
+ border: none;
2980
+ background: transparent;
2981
+ color: var(--atx-foreground);
2982
+ font: 14px var(--atx-font-ui);
2983
+ outline: none;
2984
+ }
2985
+
2986
+ .atx-media-error-more {
2987
+ margin-left: 10px;
2988
+ color: var(--atx-warning);
2989
+ font: 13px var(--atx-font-ui);
2990
+ }
2991
+
2992
+ .atx-media-error-title {
2993
+ margin: 0 0 6px;
2994
+ color: var(--atx-foreground);
2995
+ font: 500 14px var(--atx-font-ui);
2996
+ }
2997
+
2998
+ .atx-media-error-detail {
2999
+ margin: 0 0 12px;
3000
+ color: var(--atx-muted-fg);
3001
+ font: 13px/1.6 var(--atx-font-ui);
3002
+ }
3003
+
3004
+ /* The only button inside a full-width grid message, so it centres itself. */
3005
+ .atx-media-error-action {
3006
+ margin: 0 auto;
3007
+ }
3008
+
3009
+ .atx-unsplash-rate {
3010
+ margin: 14px 0 0;
3011
+ color: var(--atx-muted-fg);
3012
+ font: 11px var(--atx-font-mono);
3013
+ }
3014
+
3015
+ /* The last few requests of the hour are worth noticing. */
3016
+ .atx-unsplash-rate[data-tone='warn'] {
3017
+ color: var(--atx-warning);
3018
+ }
3019
+
3020
+ /* == Icons =================================================================
3021
+ icons.ts. One rule for every glyph in the overlay; the caller sets the size
3022
+ on the <svg> itself, so nothing about a size reaches this. */
3023
+
3024
+ .atx-ico {
3025
+ display: inline-flex;
3026
+ flex: 0 0 auto;
3027
+ align-items: center;
3028
+ justify-content: center;
3029
+ /* The spinner rotates the host, not the <svg> — an HTML element animates
3030
+ predictably where an SVG child would need transform-box juggling. */
3031
+ line-height: 0;
3032
+ }
3033
+
3034
+ /* == Small panels ==========================================================
3035
+ copy-panel.ts, notice.ts, source-popup.ts, markup.ts — four leaves that put
3036
+ one message or one control in a plain panel. [data-on] shows the popup's
3037
+ error line; [data-mono] picks the source popup's face, since both faces are
3038
+ fixed and only the choice between them is the caller's. */
3039
+
3040
+ .atx-copy-note {
3041
+ margin: 0 0 10px;
3042
+ color: var(--atx-warning);
3043
+ font: 14px/1.5 var(--atx-font-ui);
3044
+ }
3045
+
3046
+ .atx-copy-text {
3047
+ height: 300px;
3048
+ font: 12px/1.5 var(--atx-font-mono);
3049
+ white-space: pre;
3050
+ resize: vertical;
3051
+ }
3052
+
3053
+ /* Said before the reason, and about a different element than the reason is:
3054
+ muted, because it is context for what follows rather than the verdict. */
3055
+ .atx-notice-lead {
3056
+ margin: 0 0 8px;
3057
+ color: var(--atx-muted-fg);
3058
+ font: 14px/1.5 var(--atx-font-ui);
3059
+ }
3060
+
3061
+ .atx-notice-reason {
3062
+ margin: 0 0 6px;
3063
+ color: var(--atx-foreground);
3064
+ font: 14px/1.5 var(--atx-font-ui);
3065
+ }
3066
+
3067
+ /* The location line opens the source peek, so it reads as a link. */
3068
+ .atx-notice-loc {
3069
+ margin: 0 0 12px;
3070
+ color: var(--atx-muted-fg);
3071
+ font: 12px var(--atx-font-mono);
3072
+ cursor: pointer;
3073
+ }
3074
+
3075
+ .atx-notice-loc:hover {
3076
+ text-decoration: underline;
3077
+ }
3078
+
3079
+ .atx-notice-hint {
3080
+ margin: 0 0 4px;
3081
+ color: var(--atx-brand-text);
3082
+ font: 14px/1.5 var(--atx-font-ui);
3083
+ }
3084
+
3085
+ .atx-popup-label {
3086
+ display: block;
3087
+ margin-bottom: 10px;
3088
+ color: var(--atx-foreground);
3089
+ font: 500 13px var(--atx-font-ui);
3090
+ opacity: 0.8;
3091
+ }
3092
+
3093
+ .atx-popup-input {
3094
+ width: 100%;
3095
+ min-height: 120px;
3096
+ box-sizing: border-box;
3097
+ font: 14px/1.6 var(--atx-font-ui);
3098
+ white-space: pre-wrap;
3099
+ resize: vertical;
3100
+ }
3101
+
3102
+ .atx-popup-input[data-mono] {
3103
+ font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
3104
+ }
3105
+
3106
+ .atx-popup-error {
3107
+ display: none;
3108
+ margin: 10px 0 0;
3109
+ color: var(--atx-destructive);
3110
+ font: 13px/1.5 var(--atx-font-ui);
3111
+ }
3112
+
3113
+ .atx-popup-error[data-on] {
3114
+ display: block;
3115
+ }
3116
+
3117
+ .atx-markup-tags {
3118
+ display: flex;
3119
+ flex-wrap: wrap;
3120
+ align-items: center;
3121
+ gap: 4px;
3122
+ margin-top: 8px;
3123
+ }
3124
+
3125
+ .atx-markup-hint {
3126
+ margin-right: 2px;
3127
+ color: var(--atx-muted-fg);
3128
+ font: 12px/1.5 var(--atx-font-ui);
3129
+ }
3130
+
3131
+ .atx-markup-tag {
3132
+ padding: 2px 6px;
3133
+ border: 1px solid var(--atx-border);
3134
+ border-radius: var(--atx-radius-sm);
3135
+ background: var(--atx-background);
3136
+ color: var(--atx-foreground);
3137
+ font: 11px ui-monospace, SFMono-Regular, Menlo, monospace;
3138
+ cursor: pointer;
3139
+ }
3140
+
3141
+ /* == Source peek ===========================================================
3142
+ editors/peek.ts. A read-only, syntax-tinted view of the file, scrolled to
3143
+ the element's line. Every row carries the focus border so the gutter stays
3144
+ aligned; only .atx-peek-focus's is visible. [data-tone] is how the loading
3145
+ line becomes a refusal or an error, and [data-token] is one run of code. */
3146
+
3147
+ .atx-peek-code {
3148
+ max-height: 65vh;
3149
+ padding: 8px 0;
3150
+ overflow: auto;
3151
+ background: var(--atx-background);
3152
+ font: 12.5px/1.65 var(--atx-font-mono);
3153
+ }
3154
+
3155
+ .atx-peek-more {
3156
+ padding: 4px 12px 4px 15px;
3157
+ color: var(--atx-faint-fg);
3158
+ font-style: italic;
3159
+ user-select: none;
3160
+ }
3161
+
3162
+ .atx-peek-line {
3163
+ display: flex;
3164
+ border-left: 3px solid transparent;
3165
+ background: transparent;
3166
+ }
3167
+
3168
+ .atx-peek-focus {
3169
+ border-left-color: var(--atx-brand);
3170
+ background: ${hexToRgba(COLOR.brand, 0.16)};
3171
+ }
3172
+
3173
+ .atx-peek-gutter {
3174
+ flex: 0 0 auto;
3175
+ padding: 0 12px 0 0;
3176
+ color: var(--atx-faint-fg);
3177
+ text-align: right;
3178
+ user-select: none;
3179
+ }
3180
+
3181
+ .atx-peek-focus .atx-peek-gutter {
3182
+ color: var(--atx-brand-text);
3183
+ }
3184
+
3185
+ .atx-peek-text {
3186
+ flex: 1 1 auto;
3187
+ padding-right: 16px;
3188
+ white-space: pre;
3189
+ tab-size: 2;
3190
+ }
3191
+
3192
+ .atx-peek-loading {
3193
+ padding: 24px 16px;
3194
+ background: var(--atx-background);
3195
+ color: var(--atx-muted-fg);
3196
+ font: 12.5px var(--atx-font-mono);
3197
+ }
3198
+
3199
+ /* Not source but a sentence, so it wants the line-height source does not. */
3200
+ .atx-peek-loading[data-tone='warn'] {
3201
+ color: var(--atx-warning);
3202
+ line-height: 1.6;
3203
+ }
3204
+
3205
+ .atx-peek-loading[data-tone='err'] {
3206
+ color: var(--atx-destructive);
3207
+ }
3208
+
3209
+ /* Code token colours on the panel's dark ground. Chosen for contrast, not to
3210
+ mimic any one editor theme. This is the only copy of the mapping — peek.ts
3211
+ emits the kind and nothing else. */
3212
+ .atx-peek-token[data-token='plain'] { color: var(--atx-foreground); }
3213
+ .atx-peek-token[data-token='comment'] { color: var(--atx-muted-fg); font-style: italic; }
3214
+ .atx-peek-token[data-token='string'] { color: var(--atx-success-text); }
3215
+ .atx-peek-token[data-token='tag'] { color: var(--atx-brand-text); }
3216
+ .atx-peek-token[data-token='attr'] { color: var(--atx-brand-text); }
3217
+ .atx-peek-token[data-token='keyword'] { color: var(--atx-destructive); }
3218
+ .atx-peek-token[data-token='number'] { color: var(--atx-warning); }
3219
+ .atx-peek-token[data-token='fence'] { color: var(--atx-muted-fg); }
3220
+
3221
+ /* == Rules card ============================================================
3222
+ css-inspect.ts. The applied CSS rules behind one class or ID chip on the
3223
+ hover pill. Its position is measured, so only that stays inline. The
3224
+ declaration palette is deliberately the peek palette's sibling — same job,
3225
+ different vocabulary. */
3226
+
3227
+ .atx-tooltip-rules {
3228
+ position: fixed;
3229
+ max-width: 360px;
3230
+ max-height: 50vh;
3231
+ padding: 8px 10px;
3232
+ overflow-y: auto;
3233
+ border: 1px solid var(--atx-border);
3234
+ border-radius: var(--atx-radius-md);
3235
+ background: var(--atx-card);
3236
+ color: var(--atx-foreground);
3237
+ font: 12px var(--atx-font-mono);
3238
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
3239
+ cursor: default;
3240
+ }
3241
+
3242
+ .atx-tooltip-rules-empty {
3243
+ color: var(--atx-muted-fg);
3244
+ }
3245
+
3246
+ /* The first rule sits flush against the top of the card; every one after it
3247
+ is separated from the one before, which is what the hairline is for. */
3248
+ .atx-tooltip-rule {
3249
+ padding: 0 0 6px;
3250
+ }
3251
+
3252
+ .atx-tooltip-rule + .atx-tooltip-rule {
3253
+ padding: 6px 0;
3254
+ border-top: 1px solid var(--atx-border);
3255
+ }
3256
+
3257
+ .atx-tooltip-rule-sel {
3258
+ color: var(--atx-brand-text);
3259
+ word-break: break-all;
3260
+ }
3261
+
3262
+ /* 10px of breathing room above and below the properties list. */
3263
+ .atx-tooltip-rule-decl {
3264
+ margin: 10px 0;
3265
+ color: var(--atx-muted-fg);
3266
+ font: 12px var(--atx-font-mono);
3267
+ white-space: pre-wrap;
3268
+ word-break: break-word;
3269
+ }
3270
+
3271
+ .atx-tooltip-rule-foot {
3272
+ display: flex;
3273
+ align-items: center;
3274
+ justify-content: space-between;
3275
+ }
3276
+
3277
+ .atx-tooltip-rule-src {
3278
+ overflow: hidden;
3279
+ color: var(--atx-muted-fg);
3280
+ font: 10.5px var(--atx-font-mono);
3281
+ white-space: nowrap;
3282
+ text-overflow: ellipsis;
3283
+ }
3284
+
3285
+ .atx-css-token[data-css='prop'] { color: var(--atx-chart5); }
3286
+ .atx-css-token[data-css='value'] { color: var(--atx-muted-fg); }
3287
+ .atx-css-token[data-css='string'] { color: var(--atx-chart2); }
3288
+ .atx-css-token[data-css='number'] { color: var(--atx-chart3); }
3289
+ .atx-css-token[data-css='variable'] { color: var(--atx-chart1); }
3290
+ .atx-css-token[data-css='keyword'] { color: var(--atx-chart4); }
3291
+ .atx-css-token[data-css='punct'] { color: var(--atx-muted-fg); }
3292
+
3293
+ /* == Markdown body editor ==================================================
3294
+ editors/body-editor.ts. Everything here is the editor's *chrome* — the
3295
+ toolbar, the heading menu, the image panel. The writing surface itself
3296
+ (.atx-rte-content) is deliberately absent: it stays in the light DOM so
3297
+ Safari's selection and execCommand APIs can see it, and ::slotted() loses
3298
+ to the document, so its whole box lives in that module's CONTENT_CSS.
3299
+
3300
+ Three states: [data-on] on the heading menu and the image panel, which are
3301
+ both closed until asked for, and [data-hidden] on the format buttons, which
3302
+ are the half of the toolbar that means nothing in raw-markdown mode. */
3303
+
3304
+ .atx-rte-head {
3305
+ position: sticky;
3306
+ top: 0;
3307
+ z-index: 2;
3308
+ padding-bottom: 6px;
3309
+ background: var(--atx-card);
3310
+ }
3311
+
3312
+ .atx-rte-toolbar {
3313
+ display: flex;
3314
+ flex-wrap: wrap;
3315
+ align-items: center;
3316
+ gap: 2px;
3317
+ padding: 5px;
3318
+ border: 1px solid var(--atx-border);
3319
+ border-radius: var(--atx-radius-lg);
3320
+ background: var(--atx-card);
3321
+ }
3322
+
3323
+ .atx-rte-btn {
3324
+ display: inline-flex;
3325
+ align-items: center;
3326
+ justify-content: center;
3327
+ min-width: 30px;
3328
+ height: 28px;
3329
+ padding: 0 8px;
3330
+ border: none;
3331
+ border-radius: var(--atx-radius-md);
3332
+ background: transparent;
3333
+ color: var(--atx-foreground);
3334
+ font: 500 13px/1 var(--atx-font-ui);
3335
+ outline: none;
3336
+ transition: background 120ms, color 120ms;
3337
+ cursor: pointer;
3338
+ }
3339
+
3340
+ .atx-rte-btn:focus-visible {
3341
+ box-shadow: 0 0 0 2px ${hexToRgba(COLOR.ring, 0.7)};
3342
+ }
3343
+
3344
+ /* Each button wears what it does. */
3345
+ .atx-rte-btn-bold {
3346
+ font-weight: 800;
3347
+ }
3348
+
3349
+ .atx-rte-btn-italic {
3350
+ font-family: serif;
3351
+ font-style: italic;
3352
+ }
3353
+
3354
+ .atx-rte-btn-strike {
3355
+ text-decoration: line-through;
3356
+ }
3357
+
3358
+ .atx-rte-btn-pre {
3359
+ font: 700 10px var(--atx-font-mono);
3360
+ }
3361
+
3362
+ .atx-rte-btn-code {
3363
+ font: 700 13px var(--atx-font-mono);
3364
+ }
3365
+
3366
+ /* The MD/Rich toggle is the only thing on the right of the toolbar. It is the
3367
+ editor talking about itself, so it stays neutral. */
3368
+ .atx-rte-mode {
3369
+ margin-left: auto;
3370
+ font: 600 11px var(--atx-font-mono);
3371
+ letter-spacing: 0.04em;
3372
+ }
3373
+
3374
+ .atx-rte-divider {
3375
+ width: 1px;
3376
+ align-self: stretch;
3377
+ margin: 3px;
3378
+ background: var(--atx-border);
3379
+ }
3380
+
3381
+ .atx-rte-btn[data-hidden],
3382
+ .atx-rte-divider[data-hidden],
3383
+ .atx-rte-heading[data-hidden] {
3384
+ display: none;
3385
+ }
3386
+
3387
+ .atx-rte-heading {
3388
+ position: relative;
3389
+ display: inline-flex;
3390
+ }
3391
+
3392
+ .atx-rte-heading-menu {
3393
+ position: absolute;
3394
+ top: calc(100% + 4px);
3395
+ left: 0;
3396
+ z-index: 3;
3397
+ display: none;
3398
+ min-width: 150px;
3399
+ padding: 4px;
3400
+ border: 1px solid var(--atx-border);
3401
+ border-radius: var(--atx-radius-lg);
3402
+ background: var(--atx-card);
3403
+ box-shadow: 0 0 0 1px var(--atx-border), 0 8px 28px rgba(0, 0, 0, 0.4);
3404
+ }
3405
+
3406
+ .atx-rte-heading-menu[data-on] {
3407
+ display: block;
3408
+ }
3409
+
3410
+ .atx-rte-heading-item {
3411
+ display: flex;
3412
+ align-items: center;
3413
+ gap: 10px;
3414
+ width: 100%;
3415
+ padding: 6px 10px;
3416
+ border: none;
3417
+ border-radius: var(--atx-radius-md);
3418
+ background: transparent;
3419
+ color: var(--atx-foreground);
3420
+ font: 400 13px var(--atx-font-ui);
3421
+ text-align: left;
3422
+ cursor: pointer;
3423
+ }
3424
+
3425
+ .atx-rte-btn:hover,
3426
+ .atx-rte-heading-item:hover {
3427
+ background: var(--atx-accent);
3428
+ color: var(--atx-foreground);
3429
+ }
3430
+
3431
+ .atx-rte-heading-chip {
3432
+ min-width: 20px;
3433
+ font: 700 11px var(--atx-font-mono);
3434
+ opacity: 0.7;
3435
+ }
3436
+
3437
+ /* Each row previews its own level, so the size is the caller's. */
3438
+ .atx-rte-heading-name {
3439
+ font: 600 12px var(--atx-font-ui);
3440
+ }
3441
+
3442
+ /* Insert or replace an image, sharing the asset picker with the image field. */
3443
+ .atx-rte-image-panel {
3444
+ display: none;
3445
+ margin: 6px 0 0;
3446
+ padding: 12px;
3447
+ border: 1px solid var(--atx-border);
3448
+ border-radius: var(--atx-radius-lg);
3449
+ background: var(--atx-background);
3450
+ }
3451
+
3452
+ .atx-rte-image-panel[data-on] {
3453
+ display: block;
3454
+ }
3455
+
3456
+ .atx-rte-image-alt-label {
3457
+ display: block;
3458
+ margin: 10px 0 6px;
3459
+ color: var(--atx-foreground);
3460
+ font: 500 14px/1 var(--atx-font-ui);
3461
+ }
3462
+
3463
+ .atx-rte-image-actions {
3464
+ display: flex;
3465
+ justify-content: flex-end;
3466
+ gap: 8px;
3467
+ margin-top: 8px;
3468
+ }
3469
+
3470
+ /* Smaller than a footer button: these sit inside a panel inside a toolbar. */
3471
+
3472
+ /* The raw-markdown half of the MD/Rich toggle. */
3473
+ .atx-body-input {
3474
+ display: none;
3475
+ min-height: 40vh;
3476
+ padding: 12px;
3477
+ font: 400 13px/1.6 var(--atx-font-mono);
3478
+ resize: vertical;
3479
+ }
3480
+
3481
+ .atx-body-input[data-on] {
3482
+ display: block;
3483
+ }
3484
+ `;
3485
+ }