vantage-md 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,671 @@
1
+ /* vantage-md/styles/directives.css
2
+ *
3
+ * Theme layer for the `data-vantage-*` directive attributes emitted by
4
+ * rehypeVantageDirectives, and for the file-scope chrome the `vantage:`
5
+ * frontmatter key asks for (`.vantage-chip`, `.vantage-chrome`) — one file
6
+ * because the badge and the status chip are the same visual object and share
7
+ * the same rule. Plain CSS on attribute selectors and hand-written classes only:
8
+ * no Tailwind utilities, no class names computed in JS, no color a document
9
+ * can name.
10
+ *
11
+ * This file must stay UNLAYERED. Every `@tailwindcss/typography` variant
12
+ * utility flattens to exactly one class of specificity — `:where()` and
13
+ * `:not(:where(…))` both contribute zero — and all of them sit inside
14
+ * `@layer utilities`. Unlayered normal declarations outrank every layer
15
+ * regardless of specificity, which is the only reason these plain attribute
16
+ * selectors win at all. Wrapping this file in `@layer components` "to be tidy"
17
+ * makes it lose to every prose utility instead; a test guards that.
18
+ */
19
+
20
+ /* ── Tone palette (light) ───────────────────────────────────────────────── */
21
+ :root {
22
+ --vantage-tone-note-accent: #2563eb;
23
+ --vantage-tone-note-wash: rgb(37 99 235 / 0.1);
24
+ --vantage-tone-note-chip: rgb(37 99 235 / 0.12);
25
+ --vantage-tone-note-ink: #1d4ed8;
26
+
27
+ --vantage-tone-tip-accent: #15803d;
28
+ --vantage-tone-tip-wash: rgb(22 163 74 / 0.1);
29
+ --vantage-tone-tip-chip: rgb(22 163 74 / 0.12);
30
+ /* A shade past green-700, the other tones' step: green is so luminous
31
+ that green-700 over its own chip fill reads 4.39:1, under the 4.5:1 WCAG
32
+ AA asks of a chip's small text. This reads 5.0:1. */
33
+ --vantage-tone-tip-ink: #137638;
34
+
35
+ --vantage-tone-important-accent: #7c3aed;
36
+ --vantage-tone-important-wash: rgb(124 58 237 / 0.1);
37
+ --vantage-tone-important-chip: rgb(124 58 237 / 0.12);
38
+ --vantage-tone-important-ink: #6d28d9;
39
+
40
+ --vantage-tone-warning-accent: #b45309;
41
+ --vantage-tone-warning-wash: rgb(245 158 11 / 0.13);
42
+ --vantage-tone-warning-chip: rgb(245 158 11 / 0.14);
43
+ --vantage-tone-warning-ink: #b45309;
44
+
45
+ --vantage-tone-caution-accent: #dc2626;
46
+ --vantage-tone-caution-wash: rgb(220 38 38 / 0.1);
47
+ --vantage-tone-caution-chip: rgb(220 38 38 / 0.12);
48
+ --vantage-tone-caution-ink: #b91c1c;
49
+
50
+ --vantage-tone-muted-accent: #64748b;
51
+ --vantage-tone-muted-wash: rgb(100 116 139 / 0.1);
52
+ --vantage-tone-muted-chip: rgb(100 116 139 / 0.12);
53
+ --vantage-tone-muted-ink: #475569;
54
+
55
+ /* 5px, not 3px. At 3px the rule was legible when you looked at it and
56
+ invisible when you scrolled past it — and, worse, three of the six tones
57
+ were indistinguishable from each other at that width, so the treatment
58
+ said "marked" without saying *how*. Hue needs area to read as hue. */
59
+ --vantage-tone-rule-width: 5px;
60
+ --vantage-tone-rule-offset: 0.75rem;
61
+ --vantage-tone-run-bleed: 2.5rem;
62
+ /* An alert draws a real left border rather than a positioned slice, so it
63
+ gets its own width: it is a single box and has no run to line up with. */
64
+ --vantage-alert-rule-width: 4px;
65
+ }
66
+
67
+ /* ── Tone palette (dark) ────────────────────────────────────────────────── */
68
+ /* `.dark` is the app's dark selector (`@custom-variant dark` in the host
69
+ stylesheet), so a package consumer gets dark tones by toggling that class —
70
+ the same switch the rest of the app already uses. */
71
+ .dark {
72
+ --vantage-tone-note-accent: #60a5fa;
73
+ --vantage-tone-note-wash: rgb(96 165 250 / 0.16);
74
+ --vantage-tone-note-chip: rgb(59 130 246 / 0.22);
75
+ --vantage-tone-note-ink: #93c5fd;
76
+
77
+ --vantage-tone-tip-accent: #4ade80;
78
+ --vantage-tone-tip-wash: rgb(74 222 128 / 0.16);
79
+ --vantage-tone-tip-chip: rgb(34 197 94 / 0.22);
80
+ --vantage-tone-tip-ink: #86efac;
81
+
82
+ --vantage-tone-important-accent: #a78bfa;
83
+ --vantage-tone-important-wash: rgb(167 139 250 / 0.16);
84
+ --vantage-tone-important-chip: rgb(139 92 246 / 0.22);
85
+ --vantage-tone-important-ink: #c4b5fd;
86
+
87
+ --vantage-tone-warning-accent: #fbbf24;
88
+ --vantage-tone-warning-wash: rgb(245 158 11 / 0.16);
89
+ --vantage-tone-warning-chip: rgb(245 158 11 / 0.18);
90
+ --vantage-tone-warning-ink: #fcd34d;
91
+
92
+ --vantage-tone-caution-accent: #f87171;
93
+ --vantage-tone-caution-wash: rgb(248 113 113 / 0.16);
94
+ --vantage-tone-caution-chip: rgb(239 68 68 / 0.2);
95
+ --vantage-tone-caution-ink: #fca5a5;
96
+
97
+ --vantage-tone-muted-accent: #94a3b8;
98
+ --vantage-tone-muted-wash: rgb(148 163 184 / 0.14);
99
+ --vantage-tone-muted-chip: rgb(148 163 184 / 0.22);
100
+ --vantage-tone-muted-ink: #cbd5e1;
101
+ }
102
+
103
+ /* ── Token → property resolution ────────────────────────────────────────── */
104
+ /* Six rules so every rule below is tone-agnostic. An unrecognized token sets
105
+ nothing, which is what makes the treatment inert rather than gray (D2). */
106
+ [data-vantage-tone="note"] {
107
+ --vantage-tone-accent: var(--vantage-tone-note-accent);
108
+ --vantage-tone-wash: var(--vantage-tone-note-wash);
109
+ --vantage-tone-chip: var(--vantage-tone-note-chip);
110
+ --vantage-tone-ink: var(--vantage-tone-note-ink);
111
+ }
112
+ [data-vantage-tone="tip"] {
113
+ --vantage-tone-accent: var(--vantage-tone-tip-accent);
114
+ --vantage-tone-wash: var(--vantage-tone-tip-wash);
115
+ --vantage-tone-chip: var(--vantage-tone-tip-chip);
116
+ --vantage-tone-ink: var(--vantage-tone-tip-ink);
117
+ }
118
+ [data-vantage-tone="important"] {
119
+ --vantage-tone-accent: var(--vantage-tone-important-accent);
120
+ --vantage-tone-wash: var(--vantage-tone-important-wash);
121
+ --vantage-tone-chip: var(--vantage-tone-important-chip);
122
+ --vantage-tone-ink: var(--vantage-tone-important-ink);
123
+ }
124
+ [data-vantage-tone="warning"] {
125
+ --vantage-tone-accent: var(--vantage-tone-warning-accent);
126
+ --vantage-tone-wash: var(--vantage-tone-warning-wash);
127
+ --vantage-tone-chip: var(--vantage-tone-warning-chip);
128
+ --vantage-tone-ink: var(--vantage-tone-warning-ink);
129
+ }
130
+ [data-vantage-tone="caution"] {
131
+ --vantage-tone-accent: var(--vantage-tone-caution-accent);
132
+ --vantage-tone-wash: var(--vantage-tone-caution-wash);
133
+ --vantage-tone-chip: var(--vantage-tone-caution-chip);
134
+ --vantage-tone-ink: var(--vantage-tone-caution-ink);
135
+ }
136
+ [data-vantage-tone="muted"] {
137
+ --vantage-tone-accent: var(--vantage-tone-muted-accent);
138
+ --vantage-tone-wash: var(--vantage-tone-muted-wash);
139
+ --vantage-tone-chip: var(--vantage-tone-muted-chip);
140
+ --vantage-tone-ink: var(--vantage-tone-muted-ink);
141
+ }
142
+
143
+ /* ── Alert → property resolution ────────────────────────────────────────── */
144
+ /* Separate names from `--vantage-tone-*`, and the separation is a bug fix, not
145
+ tidiness. Custom properties INHERIT. A `> [!CAUTION]` inside
146
+ `<!-- vantage: section tone=important -->` is both an alert and a stamped run
147
+ member, so resolving both onto `--vantage-tone-accent` made the alert's kind
148
+ win on that element — and the section's own vertical rule turned red for the
149
+ height of the alert, and red again for the 2.5rem it bleeds upward. The
150
+ section read as three colors and looked broken. Distinct names let one
151
+ element carry both facts without either overwriting the other.
152
+
153
+ The VALUES are still the tone palette's, which is the whole point: an alert
154
+ and the tone of the same name resolve to the same four properties, so they
155
+ cannot drift and a new theme is still one custom-property block. */
156
+ [data-vantage-alert="note"] {
157
+ --vantage-alert-accent: var(--vantage-tone-note-accent);
158
+ --vantage-alert-wash: var(--vantage-tone-note-wash);
159
+ --vantage-alert-chip: var(--vantage-tone-note-chip);
160
+ --vantage-alert-ink: var(--vantage-tone-note-ink);
161
+ }
162
+ [data-vantage-alert="tip"] {
163
+ --vantage-alert-accent: var(--vantage-tone-tip-accent);
164
+ --vantage-alert-wash: var(--vantage-tone-tip-wash);
165
+ --vantage-alert-chip: var(--vantage-tone-tip-chip);
166
+ --vantage-alert-ink: var(--vantage-tone-tip-ink);
167
+ }
168
+ [data-vantage-alert="important"] {
169
+ --vantage-alert-accent: var(--vantage-tone-important-accent);
170
+ --vantage-alert-wash: var(--vantage-tone-important-wash);
171
+ --vantage-alert-chip: var(--vantage-tone-important-chip);
172
+ --vantage-alert-ink: var(--vantage-tone-important-ink);
173
+ }
174
+ [data-vantage-alert="warning"] {
175
+ --vantage-alert-accent: var(--vantage-tone-warning-accent);
176
+ --vantage-alert-wash: var(--vantage-tone-warning-wash);
177
+ --vantage-alert-chip: var(--vantage-tone-warning-chip);
178
+ --vantage-alert-ink: var(--vantage-tone-warning-ink);
179
+ }
180
+ [data-vantage-alert="caution"] {
181
+ --vantage-alert-accent: var(--vantage-tone-caution-accent);
182
+ --vantage-alert-wash: var(--vantage-tone-caution-wash);
183
+ --vantage-alert-chip: var(--vantage-tone-caution-chip);
184
+ --vantage-alert-ink: var(--vantage-tone-caution-ink);
185
+ }
186
+
187
+ /* ── The section rule ───────────────────────────────────────────────────── */
188
+ /* Every block in a section is stamped, so a per-element border+background
189
+ renders one section as N stacked boxes. Instead each member draws a slice of
190
+ one continuous vertical rule, positioned OUT of its own box (the left gutter
191
+ is already spoken for by line anchors, review highlights and blockquotes) and bled UPWARD to meet its predecessor. Bleeding only upward is
192
+ what makes the run terminate exactly at its first and last member without a
193
+ :last-of-run selector — which is unexpressible here, since members are
194
+ siblings and review mode inserts comment <div>s between them.
195
+ `--vantage-tone-heading-gutter` is set by the host app on toned headings
196
+ only; 1.5em resolves per heading level, which is the only way h2, h3 and p
197
+ share one x. Measured: the pseudo's computed `left` puts all eight block
198
+ types on the same pixel — which is a statement about where the slice would
199
+ go, NOT about whether it paints. Two of the eight clip it away entirely; the
200
+ escape below is what makes them paint, and a pixel scan is the only thing
201
+ that can tell the two apart.
202
+ `background-color` is the longhand, and the accent `var()` has no fallback,
203
+ on purpose: an unrecognized tone leaves the variable unset, the value is
204
+ invalid at computed-value time, and the rule computes to `transparent` — D2
205
+ inertness enforced by the cascade rather than by enumerating six tokens in
206
+ every selector. A "safety" fallback would paint every typo gray instead. */
207
+ /* `left` on an absolutely-positioned pseudo resolves from the PADDING edge of
208
+ its containing block, which is the member itself. So a member with a left
209
+ border of its own has its padding edge pushed inward by that border, and its
210
+ slice lands that far right of everyone else's — measured at 312 against 308
211
+ for the rest of a run, which on a 5px slice reads as a break in the rule
212
+ rather than as a bend. There is no CSS way to read an element's own border
213
+ width, so each bordered case declares the compensation, and it is declared
214
+ WHERE THE BORDER IS: this file for the alert's, the host stylesheet for the
215
+ ones Tailwind gives `blockquote` and `pre`. Same arrangement as
216
+ `--vantage-tone-heading-gutter`, and a test pins each pair equal.
217
+
218
+ The member is the slice's containing block only while it has a box. A
219
+ member written with `display: contents` has none, and its slice was placed
220
+ against an ancestor outside the scroll container: measured at the content
221
+ pane's full height, not scrolling with the document, and on a heading moved
222
+ across the pane by the heading's font size. The sanitizer refuses that
223
+ value (`DISPLAY_VALUES` in `sanitize.ts`), and
224
+ `frontend/e2e/boxless.spec.ts` measures that the slice stays in. */
225
+ [data-vantage-tone] {
226
+ position: relative;
227
+ }
228
+ [data-vantage-tone]::before {
229
+ content: "";
230
+ position: absolute;
231
+ top: 0;
232
+ bottom: 0;
233
+ left: calc(
234
+ var(--vantage-tone-heading-gutter, 0em) - var(--vantage-tone-rule-offset) -
235
+ var(--vantage-tone-border-compensation, 0px)
236
+ );
237
+ width: var(--vantage-tone-rule-width);
238
+ border-radius: var(--vantage-tone-rule-width);
239
+ background-color: var(--vantage-tone-accent);
240
+ pointer-events: none;
241
+ }
242
+ /* Positive, not `:not([data-vantage-run="start"])`: with the attribute missing
243
+ entirely (older plugin, newer CSS) this bleeds nothing and the run reads as
244
+ separate marks — degraded, never a rule hanging above the heading (D3). */
245
+ [data-vantage-tone]:is(
246
+ [data-vantage-run="middle"],
247
+ [data-vantage-run="end"]
248
+ )::before {
249
+ top: calc(-1 * var(--vantage-tone-run-bleed));
250
+ }
251
+ /* Six of the eight stampable tags compute `overflow: visible`. `pre` and `hr`
252
+ do not, and both clip the rule to NOTHING: `@tailwindcss/typography` gives
253
+ `pre` `overflow-x: auto` so wide code scrolls, the UA sheet gives `hr`
254
+ `overflow: hidden`, and CSS forces a computed `overflow` of `visible` to
255
+ `auto` on the other axis when its partner is not `visible`. So each of those
256
+ two is simultaneously the containing block for its own `::before` (the
257
+ `position: relative` above) and a clip box that discards it at the padding
258
+ edge on BOTH axes — the member paints neither its own slice nor its own
259
+ upward bleed. Measured in Chrome over the real Tailwind build of a nine-block
260
+ run: a three-line fence left one contiguous 100px void where the rule should
261
+ be, scaling linearly with the fence (168px for a 133px fence), which the
262
+ fixed 40px bleed can never repair because the void is fence height + 40px.
263
+ That is exactly the "gap taller than the bleed" `data-vantage-run` exists to
264
+ prevent, and the section reads as two sections.
265
+
266
+ So un-clip the box and re-home the scroller on the `code` child. Wide code
267
+ still scrolls horizontally — measured in a 600px viewport,
268
+ `pre.scrollWidth == pre.clientWidth == 518` while `code.scrollWidth` 752 >
269
+ `code.clientWidth` 486 and `code.scrollLeft` reaches 266 — the scrollbar sits
270
+ inside the fence's padding instead of on its border edge. The app already
271
+ sets `display: block` on `.prose pre code`; it is repeated here so the
272
+ package's own stylesheet and an external consumer get a scrollable box too.
273
+ Do NOT "tidy this away" as dead CSS: `left` computes identically with and
274
+ without it, so nothing but a pixel read can see the difference. */
275
+ [data-vantage-tone]:is(pre, hr) {
276
+ overflow: visible;
277
+ }
278
+ pre[data-vantage-tone] > code {
279
+ display: block;
280
+ overflow-x: auto;
281
+ }
282
+ /* An `hr` is 1-2px of box between two 3em margins — 48px, more than the 40px a
283
+ neighbor can bleed across — so a thematic break inside a section is the one
284
+ member whose rule also bleeds DOWNWARD, and by its own margin rather than by
285
+ the shared bleed. 3.5em, not 3em: every other member's 40px bleed overlaps its
286
+ predecessor's border edge by at least 8px, and an exact fit leaves a 1px
287
+ hairline to subpixel rounding — measured, before this was 3.5em.
288
+ Both selectors stay positive for the same D3 reason as the rule above: with
289
+ `data-vantage-run` absent nothing bleeds, and the `start` member never bleeds
290
+ up nor the `end` member down, so the run still terminates exactly at its
291
+ first and last member. */
292
+ hr[data-vantage-tone]:is(
293
+ [data-vantage-run="middle"],
294
+ [data-vantage-run="end"]
295
+ )::before {
296
+ top: -3.5em;
297
+ }
298
+ hr[data-vantage-tone]:is(
299
+ [data-vantage-run="start"],
300
+ [data-vantage-run="middle"]
301
+ )::before {
302
+ bottom: -3.5em;
303
+ }
304
+ /* An image or a drawing written as an HTML block of its own is a member like
305
+ any other — `styleRange` stamps every element in the span — but `img` and
306
+ `svg` are replaced elements, and a replaced element generates no `::before`
307
+ at all. So the rule simply stopped beside one. Measured in Chrome over the
308
+ real build: an 84px hole beside a 92px `<img>`, and 96px beside an 80px bare
309
+ `<svg>` and the row of images under it, against the 40px a neighbor bleeds.
310
+ The same drawing inside a `<div>` never had the problem, because there the
311
+ `<div>` is the member.
312
+
313
+ So these two draw their slice as a border image: one flat color, sliced so
314
+ that only the left edge region has any width, and pushed out of the box by
315
+ `border-image-outset`, which moves ink without moving layout. Both offsets
316
+ are the shared ones. They are measured from the border box, which for a
317
+ member with no border of its own is the padding box every `::before` is
318
+ measured from, so the slice lands on the same x as the rest of the run and
319
+ bleeds upward by the same amount. Not a `box-shadow` clipped to a strip: the
320
+ app rounds every image (`prose-img:rounded-lg`), and a shadow also fills the
321
+ corners outside the radius, which a clip that keeps the image cannot remove.
322
+
323
+ Drawn from the member's own box, the slice is on the rule only if that box
324
+ starts at the column's edge, and CSS cannot see where a line starts. So it is
325
+ withheld wherever the box may not. That is an image floated by `align` or
326
+ `float`, or offset by a `margin` or by `hspace`. `hspace` offsets an image
327
+ only where no stylesheet resets its margin: in the package's own viewer
328
+ `hspace="30"` moves it 30px in, but in the app Tailwind's preflight gives
329
+ every element `margin: 0`, which outranks the attribute, so there the image
330
+ stays at the edge and loses its slice all the same. A selector cannot see
331
+ which of the two it is in. It is also an image that comes after inline
332
+ content, which shares that content's line: in a row of images written on
333
+ consecutive lines that is every image but the first, whose slice already
334
+ covers the line. A gap is what these had before; a slice standing in the
335
+ middle of the column would be a new failure. The elements listed are the
336
+ inline ones the sanitizer admits, less `br`, which ends the line, and less
337
+ `svg`, `picture` and `span.katex-display`, which Tailwind's preflight,
338
+ typography and KaTeX each make a block in the app. And the slice is withheld
339
+ where the document drew a border, because a border image replaces the border
340
+ styles on all four sides: it would erase that border, not join it.
341
+ `frontend/e2e/directive_tone_rule.spec.ts` scans the images, the drawings
342
+ and the row in pixels. */
343
+ [data-vantage-tone]:is(img, svg) {
344
+ border-image-source: linear-gradient(
345
+ var(--vantage-tone-accent),
346
+ var(--vantage-tone-accent)
347
+ );
348
+ border-image-slice: 1;
349
+ border-image-width: 0 0 0 var(--vantage-tone-rule-width);
350
+ border-image-outset: 0 0 0 var(--vantage-tone-rule-offset);
351
+ }
352
+ [data-vantage-tone]:is(img, svg):is(
353
+ [data-vantage-run="middle"],
354
+ [data-vantage-run="end"]
355
+ ) {
356
+ border-image-outset: var(--vantage-tone-run-bleed) 0 0
357
+ var(--vantage-tone-rule-offset);
358
+ }
359
+ [data-vantage-tone]:is(img, svg):is(
360
+ [align="left" i],
361
+ [align="right" i],
362
+ [hspace],
363
+ [style*="float" i],
364
+ [style*="margin" i],
365
+ [style*="border" i],
366
+ [border]:not([border="0"])
367
+ ),
368
+ :is(
369
+ a,
370
+ b,
371
+ code,
372
+ del,
373
+ em,
374
+ i,
375
+ img,
376
+ input,
377
+ ins,
378
+ kbd,
379
+ math,
380
+ q,
381
+ ruby,
382
+ s,
383
+ samp,
384
+ span:not(.katex-display),
385
+ strike,
386
+ strong,
387
+ sub,
388
+ sup,
389
+ tt,
390
+ var
391
+ )
392
+ + img[data-vantage-tone] {
393
+ border-image-source: none;
394
+ }
395
+ /* A lone block has no run to stripe, so it gets the wash — this is the
396
+ `<!-- vantage: block tone=important -->` callout. `:where()` holds the
397
+ selector at one-class specificity on purpose: the review and line-anchor
398
+ backgrounds are also one class and are declared later in the host
399
+ stylesheet, so a transient state wins the tie. Writing this as
400
+ [data-vantage-tone][data-vantage-run="only"] silently eats them. */
401
+ [data-vantage-tone]:where([data-vantage-run="only"]) {
402
+ background-color: var(--vantage-tone-wash);
403
+ border-radius: 4px;
404
+ }
405
+
406
+ /* ── GFM alerts ─────────────────────────────────────────────────────────── */
407
+ /* `> [!WARNING]`, compiled to `data-vantage-alert` by `rehypeVantageAlerts`.
408
+ Reads the same four per-tone properties the `tone` vocabulary resolves, so an
409
+ alert and `<!-- vantage: block tone=warning -->` cannot drift apart and a new
410
+ theme is still one custom-property block.
411
+
412
+ Three overrides of `@tailwindcss/typography` are load-bearing, and all three
413
+ are why an unrendered alert looked as bad as it did rather than merely plain:
414
+ typography italicizes `blockquote`, grays its text, and draws `open-quote` /
415
+ `close-quote` around the first paragraph — so a callout rendered as an italic
416
+ *quotation* whose opening words were the literal `[!WARNING]`. This file is
417
+ unlayered, which is the only reason these plain attribute selectors beat the
418
+ `prose-blockquote:*` utilities; see the header. */
419
+ [data-vantage-alert] {
420
+ /* This file declares the alert's left border, so it owns the compensation
421
+ that keeps a toned alert's section slice on the run's x. */
422
+ --vantage-tone-border-compensation: var(--vantage-alert-rule-width);
423
+ margin: 1.25em 0;
424
+ padding: 0.75em 1em;
425
+ border-radius: 6px;
426
+ border: 1px solid var(--vantage-alert-chip);
427
+ border-left: var(--vantage-alert-rule-width) solid var(--vantage-alert-accent);
428
+ background-color: var(--vantage-alert-wash);
429
+ font-style: normal;
430
+ /* Typography gives `blockquote` BOTH `font-style: italic` and
431
+ `font-weight: 500`. Resetting only the italic left every alert's body a
432
+ half-step bolder than the prose around it — subtle enough to read as "this
433
+ callout is shouting" without anyone being able to say why, and measured at
434
+ 500 against 400 for an ordinary paragraph. */
435
+ font-weight: 400;
436
+ color: inherit;
437
+ /* Typography sets this on blockquotes to hang the quote mark; an alert has no
438
+ quote mark and the indent leaves its first line looking broken. */
439
+ quotes: none;
440
+ }
441
+ [data-vantage-alert] > :first-child {
442
+ margin-top: 0;
443
+ }
444
+ [data-vantage-alert] > :last-child {
445
+ margin-bottom: 0;
446
+ }
447
+ /* The generated quotation marks. `content: none` rather than `""` so the
448
+ pseudo-element is not generated at all — typography also gives it a
449
+ `margin-right`, which an empty string would keep. */
450
+ [data-vantage-alert] p:first-of-type::before,
451
+ [data-vantage-alert] p:last-of-type::after {
452
+ content: none;
453
+ }
454
+ /* The title is a real element, not CSS `content`: this plugin runs in the shared
455
+ pipeline, so every renderer emits it — unlike the collapse caret, which app
456
+ JS injects and therefore had to stay out of the document's text. A `div`
457
+ rather than a `p` so it carries no `data-source-line` and cannot become the
458
+ block a review comment anchors to. */
459
+ .vantage-alert-title {
460
+ margin: 0 0 0.25em;
461
+ color: var(--vantage-alert-ink);
462
+ font-weight: 600;
463
+ font-style: normal;
464
+ line-height: 1.5;
465
+ }
466
+
467
+ /* ── Emphasis ───────────────────────────────────────────────────────────── */
468
+ /* `normal` is the absence of a rule — no declaration needed, and none written.
469
+ Emphasis modulates the tone rule rather than drawing one of its own, so
470
+ "what it is" and "how loud" stay separable and the geometry stays in one
471
+ place. Headings, pre and table are excluded from the weight bump because
472
+ this stylesheet is unlayered and would otherwise beat
473
+ `prose-headings:font-semibold` and de-bold a toned heading to 500. */
474
+ [data-vantage-emphasis="strong"] {
475
+ --vantage-tone-rule-width: 6px;
476
+ }
477
+ [data-vantage-emphasis="strong"]:not(:is(h1, h2, h3, h4, h5, h6, pre, table)) {
478
+ font-weight: 500;
479
+ }
480
+ /* `quiet` is quiet, not invisible. It used to fade the whole block to 0.72,
481
+ which on muted gray text put the result close to the page and made a
482
+ de-emphasized paragraph genuinely hard to read — a legibility loss, which is
483
+ nearer content loss than decoration, and the same reason print restores it.
484
+ The volume now lives mostly in the rule width, where it costs no contrast:
485
+ 4px against `normal`'s 5px and `strong`'s 6px. The remaining fade is slight
486
+ and deliberate. */
487
+ [data-vantage-emphasis="quiet"] {
488
+ --vantage-tone-rule-width: 4px;
489
+ opacity: 0.88;
490
+ }
491
+
492
+ /* ── The chip ───────────────────────────────────────────────────────────── */
493
+ /* One rule, two subjects: the `badge=` chip can only be a pseudo-element
494
+ (stamp, don't wrap), and the frontmatter status chip is a real element. A
495
+ pseudo cannot take a class, so the geometry is shared by selector list.
496
+ Numbers lifted from the app's `.review-status-badge` so the two chips in the
497
+ product are the same object. */
498
+ .vantage-chip,
499
+ [data-vantage-badge]::after {
500
+ display: inline-block;
501
+ padding: 1px 8px;
502
+ border-radius: 4px;
503
+ font-family: inherit;
504
+ font-size: var(--vantage-chip-size, 11px);
505
+ font-weight: 600;
506
+ line-height: 1.6;
507
+ letter-spacing: 0.05em;
508
+ text-transform: uppercase;
509
+ white-space: nowrap;
510
+ vertical-align: 0.12em;
511
+ color: var(--vantage-chip-ink, var(--vantage-tone-muted-ink));
512
+ background-color: var(--vantage-chip-wash, var(--vantage-tone-muted-chip));
513
+ }
514
+ /* The badge scales with its heading; the standalone chip stays 11px. */
515
+ [data-vantage-badge]::after {
516
+ content: attr(data-vantage-badge);
517
+ margin-left: 0.5em;
518
+ font-weight: 600;
519
+ --vantage-chip-size: clamp(10px, 0.5em, 12px);
520
+ }
521
+ [data-vantage-badge="wip"]::after,
522
+ .vantage-chip--note {
523
+ --vantage-chip-ink: var(--vantage-tone-note-ink);
524
+ --vantage-chip-wash: var(--vantage-tone-note-chip);
525
+ }
526
+ [data-vantage-badge="done"]::after,
527
+ .vantage-chip--tip {
528
+ --vantage-chip-ink: var(--vantage-tone-tip-ink);
529
+ --vantage-chip-wash: var(--vantage-tone-tip-chip);
530
+ }
531
+ [data-vantage-badge="stale"]::after,
532
+ .vantage-chip--warning {
533
+ --vantage-chip-ink: var(--vantage-tone-warning-ink);
534
+ --vantage-chip-wash: var(--vantage-tone-warning-chip);
535
+ }
536
+ [data-vantage-badge="blocked"]::after,
537
+ .vantage-chip--caution {
538
+ --vantage-chip-ink: var(--vantage-tone-caution-ink);
539
+ --vantage-chip-wash: var(--vantage-tone-caution-chip);
540
+ }
541
+ [data-vantage-badge="draft"]::after,
542
+ .vantage-chip--muted {
543
+ --vantage-chip-ink: var(--vantage-tone-muted-ink);
544
+ --vantage-chip-wash: var(--vantage-tone-muted-chip);
545
+ }
546
+ /* No badge word maps onto `important` — the badge vocabulary is five words and
547
+ the tone vocabulary is six — but a chip consumer names a tone, not a badge,
548
+ so the modifier exists for all six. One per tone, guarded by a test. */
549
+ .vantage-chip--important {
550
+ --vantage-chip-ink: var(--vantage-tone-important-ink);
551
+ --vantage-chip-wash: var(--vantage-tone-important-chip);
552
+ }
553
+
554
+ /* ── File-scope chrome ──────────────────────────────────────────────────── */
555
+ /* The strip the document status chip sits in — the first element of the content
556
+ column, above the metadata card (`vantage:` frontmatter, §4.5). A row rather
557
+ than a bare chip because this is where any later file-scoped chrome joins it,
558
+ and because the flow needs the gap to the card whether or not a card follows.
559
+ `text-transform` on `.vantage-chip` above is what makes the chip read as a
560
+ label while the element's text content stays the lowercase token every other
561
+ renderer would see (D5). */
562
+ .vantage-chrome {
563
+ display: flex;
564
+ align-items: center;
565
+ gap: 0.5rem;
566
+ margin-bottom: 0.75rem;
567
+ }
568
+
569
+ /* ── Collapse ───────────────────────────────────────────────────────────── */
570
+ /* The hiding rule is TRIPLE-GATED, and all three gates are load-bearing.
571
+
572
+ `[data-vantage-collapse-ready]` is set on the prose container by the toggle
573
+ JS, *after* it has attached its handlers. A renderer without that JS — the CLI
574
+ checker's `renderMarkdown` HTML, an external consumer of this stylesheet —
575
+ never sets it, so nothing is hidden and the whole document reads. Hidden
576
+ content with no way to reveal it is content loss, not a style (P1/D8).
577
+
578
+ `[data-vantage-collapse-armed]` is the same invariant one block at a time, and
579
+ the container marker is not a substitute for it: "a control exists somewhere in
580
+ this document" is not "this block can be reopened". The toggle JS arms only the
581
+ blocks of a group it actually gave a caret, so a group whose toggle is missing
582
+ or unusable — and a bare `data-vantage-collapsed="true"` written in raw HTML
583
+ with no group at all, which the sanitizer allows by name and value — stays on
584
+ the page instead of becoming text nothing can reveal. It is deliberately an
585
+ attribute no document can write.
586
+
587
+ `@media not print` is the third gate, and it is not the same thing as a
588
+ `display: revert` counter-rule: `not print` means this declaration does not
589
+ exist in the print stylesheet, so no third rule can defeat it. A collapsed
590
+ section that printed closed would be content loss on paper (D7).
591
+
592
+ Residual, recorded honestly: `none` is one of the sanitizer's
593
+ `DISPLAY_VALUES`, so a document can still hide its own block with
594
+ `style="display: none"`. That allowance, not this rule, is the boundary — what
595
+ these gates guarantee is that nothing *this pipeline* stamps is hidden without
596
+ a control that opens it. */
597
+ @media not print {
598
+ [data-vantage-collapse-ready]
599
+ [data-vantage-collapsed="true"][data-vantage-collapse-armed] {
600
+ display: none;
601
+ }
602
+ }
603
+ /* The caret is a REAL element the toggle JS injects, not a third pseudo:
604
+ `::before` is already the tone rule and `::after` the badge, and one heading
605
+ can carry all three. It is a `<button>` so that it is keyboard-operable, so
606
+ that `aria-expanded` sits on an element allowed to carry it, and — measured —
607
+ because the app's review-mode click handler bails on `button`, which is what
608
+ keeps a toggle click from also opening the comment popover.
609
+
610
+ The glyph is drawn here rather than written into the element's text: the
611
+ document's text content then stays byte-identical to what every other renderer
612
+ produces, so no block hash, no clipboard payload and no anchor shifts because
613
+ the JS ran (D5). Note the heading itself deliberately gets no `cursor:
614
+ pointer` — it is not the control, and in review mode a click on it belongs to
615
+ the comment popover. */
616
+ .vantage-collapse-caret {
617
+ display: inline-block;
618
+ width: 1em;
619
+ margin: 0 0.15em 0 0;
620
+ padding: 0;
621
+ border: 0;
622
+ background-color: transparent;
623
+ color: var(--vantage-tone-muted-accent);
624
+ font-size: 0.7em;
625
+ line-height: 1;
626
+ vertical-align: 0.15em;
627
+ cursor: pointer;
628
+ transition: transform 0.15s;
629
+ }
630
+ .vantage-collapse-caret::before {
631
+ content: "\25be";
632
+ }
633
+ [data-vantage-collapse-toggle] .vantage-collapse-caret[aria-expanded="false"] {
634
+ transform: rotate(-90deg);
635
+ }
636
+
637
+ /* ── Print (D7) ─────────────────────────────────────────────────────────── */
638
+ /* Decoration goes to ink-cheap monochrome; nothing informational is dropped.
639
+ `quiet` is restored to full opacity — a faded print is a legibility loss,
640
+ which is nearer content loss than decoration. #57606a is the gray the host
641
+ app's own print block already uses. Tone never sets `color`: the host's
642
+ print block forces `.prose, .prose *` to #1a1a1a with `!important`, so a
643
+ tone color would vanish in one of the three renderings D5 covers. */
644
+ @media print {
645
+ [data-vantage-tone]::before {
646
+ background-color: #57606a;
647
+ }
648
+ /* The same gray for the slice an image or a drawing draws itself. Less
649
+ specific than the rule that withholds the slice, which still wins here. */
650
+ [data-vantage-tone]:is(img, svg) {
651
+ border-image-source: linear-gradient(#57606a, #57606a);
652
+ }
653
+ [data-vantage-tone]:where([data-vantage-run="only"]) {
654
+ background-color: transparent;
655
+ }
656
+ [data-vantage-emphasis="quiet"] {
657
+ opacity: 1;
658
+ }
659
+ .vantage-chip,
660
+ [data-vantage-badge]::after {
661
+ padding: 0 6px;
662
+ border: 1px solid #57606a;
663
+ background-color: transparent;
664
+ color: #1a1a1a;
665
+ }
666
+ /* Every collapsed block prints (the rule above simply does not exist here), so
667
+ the caret would print as a control that cannot be operated on paper. */
668
+ .vantage-collapse-caret {
669
+ display: none;
670
+ }
671
+ }