@axiapps/axi-design 1.46.0 → 1.48.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.
package/dist/axi.css CHANGED
@@ -727,6 +727,152 @@ button.axi-panel--tile:hover,
727
727
  .axi-btn--xs { --axi-btn-pad: 5px 9px; --axi-btn-size: 11px; }
728
728
  .axi-btn--icon { --axi-btn-pad: 7px; }
729
729
 
730
+ /* ---------- split button ---------- */
731
+ /* One control with two targets: a primary action and a disclosure half that
732
+ opens a menu of its variants. The name is `.axi-btn-split` and not
733
+ `.axi-split` because that one is already the split *pane* in
734
+ src/layout.css - a pane and a button are not the same noun, and the
735
+ language only gets one of them per name.
736
+
737
+ The whole component exists because a consumer hand-wrote it and got the
738
+ radius wrong, which was not carelessness: a split button is the one place
739
+ where a corner is sometimes square and sometimes the surface's, so it is
740
+ the one place where a literal gets written. That consumer's control was
741
+ rounded at 10px on all three surfaces, correct on glass and wrong on the
742
+ other two, while every ordinary .axi-btn beside it followed
743
+ --axi-radius-sm. A joined control is not hard, it is just impossible to
744
+ get right once per app.
745
+
746
+ The requirement the consumer stated is the one the rules below are built
747
+ to: it has to feel like one button on all styles. Not two buttons placed
748
+ carefully - one control that happens to have a seam. Everything here
749
+ follows from that, and the three surfaces disagree about enough
750
+ (--axi-border-control is 3px on axi and 1px on flat and glass,
751
+ --axi-shadow-control is a hard offset block on axi and a soft drop
752
+ elsewhere) that "looks joined" had to be true by construction rather than
753
+ by tuning. */
754
+ .axi-btn-split {
755
+ position: relative;
756
+ display: inline-flex;
757
+ /* stretch, not center: the two halves hold different content - a word and a
758
+ glyph - so content-sized heights drift apart by a pixel or two and the
759
+ seam stops being straight. */
760
+ align-items: stretch;
761
+ /* The wrapper moves and the halves do not, which is what makes the hover a
762
+ property of the control rather than of whichever half the cursor found. */
763
+ transition: transform .1s;
764
+ }
765
+ /* Corners: square at the seam, the surface's radius on the outside. Logical
766
+ properties rather than the `border-radius: 0 10px 10px 0` shorthand, so an
767
+ RTL consumer gets a control joined on the correct side without restating
768
+ anything - and so that each corner is set exactly once. The shorthand is
769
+ how the hand-written version ended up needing a comment explaining that
770
+ the rule above it had already squared both right corners. */
771
+ .axi-btn-split > .axi-btn { border-radius: 0; }
772
+ .axi-btn-split > .axi-btn:first-child {
773
+ border-start-start-radius: var(--axi-radius-sm);
774
+ border-end-start-radius: var(--axi-radius-sm);
775
+ }
776
+ .axi-btn-split > .axi-btn:last-child {
777
+ border-start-end-radius: var(--axi-radius-sm);
778
+ border-end-end-radius: var(--axi-radius-sm);
779
+ }
780
+ /* The seam is one shared edge, not a gap and not a drawn line. Pulling the
781
+ second half back by the control border width lands its left border exactly
782
+ on the first half's right border, so the two become a single stroke at the
783
+ language's own weight, in the language's own colour, on every surface and
784
+ under every ink. A 1px margin instead - which is what the hand-written
785
+ version used - puts two borders and a sliver of background between the
786
+ halves: 7px of edge on the default surface where the border is 3px, which
787
+ is why that control read as two buttons touching.
788
+
789
+ Negative margin and not `border-inline-start: 0`, because dropping the
790
+ border would make the toggle one border-width shorter than its sibling and
791
+ the stretch above would have to fight it. */
792
+ .axi-btn-split > .axi-btn + .axi-btn {
793
+ margin-inline-start: calc(var(--axi-border-control) * -1);
794
+ }
795
+ /* A filled half needs the seam drawn in the ink that sits ON the fill, not in
796
+ the line that sits around it. --axi-ink-line is opaque near-black on the
797
+ main theme, where it reads as a stroke against anything; on flat and glass
798
+ it is a light translucent edge - rgba(255,255,255,.10) and
799
+ rgba(180,200,255,.16) - chosen to catch light against a dark surface, which
800
+ is every other place the language draws it. Laid across two bright accent
801
+ fields it is not faint, it is the wrong direction: a lightener on a light
802
+ fill can never resolve into an edge, so the shared stroke was there and
803
+ simply could not be seen, and the control read as one undivided lozenge.
804
+
805
+ --axi-accent-ink is the language's answer to "what reads on top of the
806
+ accent" - it is what the label is already drawn in - and it is near-black on
807
+ all three surfaces. On the main theme it is literally --axi-ink-line, so
808
+ this changes nothing there and the 3px seam is the same stroke it was.
809
+
810
+ Either half being filled is enough: the stroke you see is the second half's
811
+ border, and a dark hairline reads against the plain fill on the other side
812
+ of it just as well. */
813
+ .axi-btn-split > .axi-btn--primary + .axi-btn,
814
+ .axi-btn-split > .axi-btn + .axi-btn--primary {
815
+ border-inline-start-color: var(--axi-accent-ink);
816
+ }
817
+ /* Neither half casts a shadow across the seam. The surfaces that spell their
818
+ relief as a soft drop give every control a blur that reaches outward on all
819
+ four sides - glass's is `0 6px 16px`, so it extends roughly 8px sideways -
820
+ and a half of a joined control has a sibling 8px away rather than the page.
821
+ The toggle paints after the label half, so its leftward bleed lands on top
822
+ of its neighbour as a dark vertical band just inside the seam, which is the
823
+ second way this control could read as two buttons rather than one. The
824
+ label half bleeds the other way for the same reason, and shows wherever the
825
+ toggle's fill is translucent - which on glass is every tone but the ink'd
826
+ ones.
827
+
828
+ So each half clips its own shadow flush with the edge it shares, and leaves
829
+ the other three sides alone to drop as far as the surface says. The clip is
830
+ a no-op on the main theme, whose block is a hard offset that only ever
831
+ travels right and down, and nearly one on flat; it does its work exactly
832
+ where the blur is wide enough to cross.
833
+
834
+ -100% is the box's own width or height, which is always more slack than a
835
+ control-sized blur needs, and stays right if a surface ever spells a bigger
836
+ one - a literal here would be a number to maintain.
837
+
838
+ `inset()` is physical, so RTL restates the pair rather than inheriting a
839
+ clip on the wrong edges; this is the one place in the component where the
840
+ logical properties above cannot carry the direction.
841
+
842
+ Not while focused: the focus ring is an `outline` drawn 2px outside the box,
843
+ so a clip flush with that edge would cut it in half on the seam side. A
844
+ keyboard user seeing a complete ring is worth more than the band. */
845
+ .axi-btn-split > .axi-btn:where(:not(:focus-visible)):first-child { clip-path: inset(-100% 0 -100% -100%); }
846
+ .axi-btn-split > .axi-btn:where(:not(:focus-visible)):last-child { clip-path: inset(-100% -100% -100% 0); }
847
+ .axi-btn-split:dir(rtl) > .axi-btn:where(:not(:focus-visible)):first-child { clip-path: inset(-100% -100% -100% 0); }
848
+ .axi-btn-split:dir(rtl) > .axi-btn:where(:not(:focus-visible)):last-child { clip-path: inset(-100% 0 -100% -100%); }
849
+ /* Hover is the whole control's. The generic .axi-btn hover translates the
850
+ button and deepens its block; left alone, that moves one half of a joined
851
+ control two pixels up and away from the other and tears the seam open -
852
+ the most visible way this component could fail at being one button.
853
+ So: the translate moves to the wrapper, and both halves take the hover
854
+ block whichever one the cursor is over.
855
+
856
+ What stays per-half is colour. The generic hover's dim-to-plain brighten
857
+ still lands only on the half under the cursor, which is the whole reason a
858
+ split button has two targets - the control lifts as one, and the reader can
859
+ still see which action they are about to take.
860
+
861
+ Specificity: these two rules weigh the same as `.axi-btn--primary:hover`
862
+ and win on source order, which is the ordering this section depends on -
863
+ it must stay below the button section it adjusts. The `:where()` keeps the
864
+ wrapper's hover from spending a point, so the ink layer still out-ranks
865
+ everything here, per rule 6. */
866
+ .axi-btn-split:where(:hover) { transform: translate(-2px, -2px); }
867
+ .axi-btn-split > .axi-btn:where(:hover) { transform: none; }
868
+ .axi-btn-split:where(:hover) > .axi-btn { box-shadow: var(--axi-shadow-control); }
869
+ .axi-btn-split:where(:hover) > .axi-btn--primary { box-shadow: var(--axi-shadow-control-hover); }
870
+ /* The disclosure half. Vertical padding is zero because the stretch above
871
+ already gave it the label half's height, and 20px sides around a single
872
+ chevron is the wide-rectangle failure --icon exists to fix. The gap is for
873
+ the consumers that put a status dot beside the chevron. */
874
+ .axi-btn-split__toggle { --axi-btn-pad: 0 11px; gap: 5px; }
875
+
730
876
  /* ---------- pill ---------- */
731
877
  /* A filter toggle. Its pressed state fills with whatever colour the consumer
732
878
  has put on it, so the control reads as the thing it filters to rather than
@@ -2516,6 +2662,12 @@ textarea.axi-input {
2516
2662
  display: flex; align-items: center; gap: 12px;
2517
2663
  height: 38px; padding-left: 14px;
2518
2664
  background: var(--axi-ground-deep);
2665
+ /* The chrome takes the surface filter for the same reason a panel does: on a
2666
+ theme that makes its surfaces translucent, the one strip a reader never
2667
+ scrolls away is the one that has to agree. --axi-surface-filter is none on
2668
+ every opaque surface, so this is inert there - the same inert-by-default
2669
+ shape the panels already use. */
2670
+ backdrop-filter: var(--axi-surface-filter);
2519
2671
  border-bottom: var(--axi-border-control) solid var(--axi-ink-line);
2520
2672
  font: var(--axi-t-micro);
2521
2673
  letter-spacing: var(--axi-ls-micro);
@@ -2540,6 +2692,13 @@ textarea.axi-input {
2540
2692
  /* Close is the one destructive control in the chrome, and the one place a
2541
2693
  status ink belongs in a titlebar. */
2542
2694
  .axi-titlebar__btns :where(button:last-child:hover) { background: var(--axi-danger); color: var(--axi-ink-on-fill); }
2695
+ /* The page-wide ring sits 2px OUTSIDE its element, which a window button
2696
+ cannot afford: it is flush to the top of the frame and flush to its
2697
+ neighbours, so an outward ring spills past the window edge and reads as a
2698
+ loud accent box across the corner of the chrome. Pull it inward, the same
2699
+ way a picker option and a select option already do inside their own tight
2700
+ containers. */
2701
+ .axi-titlebar__btns :where(button:focus-visible) { outline-offset: -3px; }
2543
2702
  /* The scrolling region under the chrome. */
2544
2703
  .axi-window__body { flex: 1; overflow-y: auto; }
2545
2704
 
@@ -17,6 +17,10 @@
17
17
  --axi-text-dim: #8b929e;
18
18
  --axi-text-faint: #5d6577;
19
19
  --axi-scrim: rgba(4, 5, 8, .72);
20
+ --axi-offset-panel: 0;
21
+ --axi-offset-control: 0;
22
+ --axi-offset-panel-hover: 0;
23
+ --axi-offset-control-hover: 0;
20
24
  --axi-shadow-panel: 0 2px 8px rgba(0, 0, 0, .4), 0 1px 3px rgba(0, 0, 0, .3), inset 0 1px 0 rgba(255, 255, 255, .05);
21
25
  --axi-shadow-control: 0 1px 4px rgba(0, 0, 0, .3), inset 0 1px 0 rgba(255, 255, 255, .045);
22
26
  --axi-shadow-panel-hover: 0 6px 18px rgba(0, 0, 0, .5), 0 2px 5px rgba(0, 0, 0, .35), inset 0 1px 0 rgba(255, 255, 255, .07);
@@ -13,7 +13,7 @@
13
13
  --axi-surface-filter: blur(18px) saturate(140%);
14
14
  --axi-ink-line: rgba(180, 200, 255, .16);
15
15
  --axi-rule: rgba(180, 200, 255, .14);
16
- --axi-ground-deep: #0c0e12;
16
+ --axi-ground-deep: rgba(12, 14, 18, .72);
17
17
  --axi-well-fill: rgba(6, 8, 14, .32);
18
18
  --axi-accent-ink: #0c0e12;
19
19
  --axi-ink-on-fill: #0c0e12;
@@ -21,6 +21,10 @@
21
21
  --axi-text-dim: #b3bdd0;
22
22
  --axi-text-faint: #7f8aa0;
23
23
  --axi-scrim: rgba(4, 6, 12, .66);
24
+ --axi-offset-panel: 0;
25
+ --axi-offset-control: 0;
26
+ --axi-offset-panel-hover: 0;
27
+ --axi-offset-control-hover: 0;
24
28
  --axi-shadow-panel: 0 18px 44px rgba(0, 0, 0, .55), inset 0 1px 0 rgba(255, 255, 255, .14);
25
29
  --axi-shadow-control: 0 6px 16px rgba(0, 0, 0, .45), inset 0 1px 0 rgba(255, 255, 255, .12);
26
30
  --axi-shadow-panel-hover: 0 26px 60px rgba(0, 0, 0, .6), inset 0 1px 0 rgba(255, 255, 255, .2);
package/docs/RULES.md CHANGED
@@ -830,10 +830,19 @@ What is **not** a theme's to restate, and each for its own reason:
830
830
  companions are not on this list: `--axi-accent-ink` and `--axi-ink-on-fill`
831
831
  are the colour a word is written in when it sits on a fill, and a theme that
832
832
  lightens the outline has to be able to hold them dark.
833
- - **The offset scale.** `--axi-offset-*` is what the main theme's block is
834
- composed *from*. A theme restates the composed `--axi-shadow-*` wholesale or
835
- leaves it; retuning the offsets underneath would leave the two spellings of
836
- "the block" disagreeing.
833
+ - **The offset scale, except to switch it off.** `--axi-offset-*` is what the
834
+ main theme's block is composed *from*, and retuning it underneath a restated
835
+ `--axi-shadow-*` would leave the two spellings of "the block" disagreeing.
836
+ A theme may set the offsets to `0` and may do nothing else to them. Zero is
837
+ not a retune — it is the same sentence the restated `--axi-shadow-*` is
838
+ already saying. This started out as a flat prohibition and shipped a bug:
839
+ the tokens are public, consumers compose blocks from them directly rather
840
+ than asking for `--axi-shadow-panel`, and a theme whose relief is a soft drop
841
+ was leaving a 6px near-black step standing behind every one of those cards.
842
+ On a square corner it read as a stray backdrop; on a rounded one it read as
843
+ broken rounding. A consumer still composing from the offsets now gets no
844
+ relief on a theme rather than the wrong one, which is a visible prompt to ask
845
+ for the composed block instead.
837
846
  - **The measure.** `--axi-page`, `--axi-gutter`. Where the text wraps is not a
838
847
  look, and moving it would reflow every page rather than repaint it.
839
848
  - **The type.** `--axi-sans`, `--axi-mono`, `--axi-t-*`, `--axi-ls-*`. The same
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiapps/axi-design",
3
- "version": "1.46.0",
3
+ "version": "1.48.0",
4
4
  "description": "The design language for the axi suite — flat and outlined, dark, drawn in saturated ink.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -323,6 +323,152 @@ button.axi-panel--tile:hover,
323
323
  .axi-btn--xs { --axi-btn-pad: 5px 9px; --axi-btn-size: 11px; }
324
324
  .axi-btn--icon { --axi-btn-pad: 7px; }
325
325
 
326
+ /* ---------- split button ---------- */
327
+ /* One control with two targets: a primary action and a disclosure half that
328
+ opens a menu of its variants. The name is `.axi-btn-split` and not
329
+ `.axi-split` because that one is already the split *pane* in
330
+ src/layout.css - a pane and a button are not the same noun, and the
331
+ language only gets one of them per name.
332
+
333
+ The whole component exists because a consumer hand-wrote it and got the
334
+ radius wrong, which was not carelessness: a split button is the one place
335
+ where a corner is sometimes square and sometimes the surface's, so it is
336
+ the one place where a literal gets written. That consumer's control was
337
+ rounded at 10px on all three surfaces, correct on glass and wrong on the
338
+ other two, while every ordinary .axi-btn beside it followed
339
+ --axi-radius-sm. A joined control is not hard, it is just impossible to
340
+ get right once per app.
341
+
342
+ The requirement the consumer stated is the one the rules below are built
343
+ to: it has to feel like one button on all styles. Not two buttons placed
344
+ carefully - one control that happens to have a seam. Everything here
345
+ follows from that, and the three surfaces disagree about enough
346
+ (--axi-border-control is 3px on axi and 1px on flat and glass,
347
+ --axi-shadow-control is a hard offset block on axi and a soft drop
348
+ elsewhere) that "looks joined" had to be true by construction rather than
349
+ by tuning. */
350
+ .axi-btn-split {
351
+ position: relative;
352
+ display: inline-flex;
353
+ /* stretch, not center: the two halves hold different content - a word and a
354
+ glyph - so content-sized heights drift apart by a pixel or two and the
355
+ seam stops being straight. */
356
+ align-items: stretch;
357
+ /* The wrapper moves and the halves do not, which is what makes the hover a
358
+ property of the control rather than of whichever half the cursor found. */
359
+ transition: transform .1s;
360
+ }
361
+ /* Corners: square at the seam, the surface's radius on the outside. Logical
362
+ properties rather than the `border-radius: 0 10px 10px 0` shorthand, so an
363
+ RTL consumer gets a control joined on the correct side without restating
364
+ anything - and so that each corner is set exactly once. The shorthand is
365
+ how the hand-written version ended up needing a comment explaining that
366
+ the rule above it had already squared both right corners. */
367
+ .axi-btn-split > .axi-btn { border-radius: 0; }
368
+ .axi-btn-split > .axi-btn:first-child {
369
+ border-start-start-radius: var(--axi-radius-sm);
370
+ border-end-start-radius: var(--axi-radius-sm);
371
+ }
372
+ .axi-btn-split > .axi-btn:last-child {
373
+ border-start-end-radius: var(--axi-radius-sm);
374
+ border-end-end-radius: var(--axi-radius-sm);
375
+ }
376
+ /* The seam is one shared edge, not a gap and not a drawn line. Pulling the
377
+ second half back by the control border width lands its left border exactly
378
+ on the first half's right border, so the two become a single stroke at the
379
+ language's own weight, in the language's own colour, on every surface and
380
+ under every ink. A 1px margin instead - which is what the hand-written
381
+ version used - puts two borders and a sliver of background between the
382
+ halves: 7px of edge on the default surface where the border is 3px, which
383
+ is why that control read as two buttons touching.
384
+
385
+ Negative margin and not `border-inline-start: 0`, because dropping the
386
+ border would make the toggle one border-width shorter than its sibling and
387
+ the stretch above would have to fight it. */
388
+ .axi-btn-split > .axi-btn + .axi-btn {
389
+ margin-inline-start: calc(var(--axi-border-control) * -1);
390
+ }
391
+ /* A filled half needs the seam drawn in the ink that sits ON the fill, not in
392
+ the line that sits around it. --axi-ink-line is opaque near-black on the
393
+ main theme, where it reads as a stroke against anything; on flat and glass
394
+ it is a light translucent edge - rgba(255,255,255,.10) and
395
+ rgba(180,200,255,.16) - chosen to catch light against a dark surface, which
396
+ is every other place the language draws it. Laid across two bright accent
397
+ fields it is not faint, it is the wrong direction: a lightener on a light
398
+ fill can never resolve into an edge, so the shared stroke was there and
399
+ simply could not be seen, and the control read as one undivided lozenge.
400
+
401
+ --axi-accent-ink is the language's answer to "what reads on top of the
402
+ accent" - it is what the label is already drawn in - and it is near-black on
403
+ all three surfaces. On the main theme it is literally --axi-ink-line, so
404
+ this changes nothing there and the 3px seam is the same stroke it was.
405
+
406
+ Either half being filled is enough: the stroke you see is the second half's
407
+ border, and a dark hairline reads against the plain fill on the other side
408
+ of it just as well. */
409
+ .axi-btn-split > .axi-btn--primary + .axi-btn,
410
+ .axi-btn-split > .axi-btn + .axi-btn--primary {
411
+ border-inline-start-color: var(--axi-accent-ink);
412
+ }
413
+ /* Neither half casts a shadow across the seam. The surfaces that spell their
414
+ relief as a soft drop give every control a blur that reaches outward on all
415
+ four sides - glass's is `0 6px 16px`, so it extends roughly 8px sideways -
416
+ and a half of a joined control has a sibling 8px away rather than the page.
417
+ The toggle paints after the label half, so its leftward bleed lands on top
418
+ of its neighbour as a dark vertical band just inside the seam, which is the
419
+ second way this control could read as two buttons rather than one. The
420
+ label half bleeds the other way for the same reason, and shows wherever the
421
+ toggle's fill is translucent - which on glass is every tone but the ink'd
422
+ ones.
423
+
424
+ So each half clips its own shadow flush with the edge it shares, and leaves
425
+ the other three sides alone to drop as far as the surface says. The clip is
426
+ a no-op on the main theme, whose block is a hard offset that only ever
427
+ travels right and down, and nearly one on flat; it does its work exactly
428
+ where the blur is wide enough to cross.
429
+
430
+ -100% is the box's own width or height, which is always more slack than a
431
+ control-sized blur needs, and stays right if a surface ever spells a bigger
432
+ one - a literal here would be a number to maintain.
433
+
434
+ `inset()` is physical, so RTL restates the pair rather than inheriting a
435
+ clip on the wrong edges; this is the one place in the component where the
436
+ logical properties above cannot carry the direction.
437
+
438
+ Not while focused: the focus ring is an `outline` drawn 2px outside the box,
439
+ so a clip flush with that edge would cut it in half on the seam side. A
440
+ keyboard user seeing a complete ring is worth more than the band. */
441
+ .axi-btn-split > .axi-btn:where(:not(:focus-visible)):first-child { clip-path: inset(-100% 0 -100% -100%); }
442
+ .axi-btn-split > .axi-btn:where(:not(:focus-visible)):last-child { clip-path: inset(-100% -100% -100% 0); }
443
+ .axi-btn-split:dir(rtl) > .axi-btn:where(:not(:focus-visible)):first-child { clip-path: inset(-100% -100% -100% 0); }
444
+ .axi-btn-split:dir(rtl) > .axi-btn:where(:not(:focus-visible)):last-child { clip-path: inset(-100% 0 -100% -100%); }
445
+ /* Hover is the whole control's. The generic .axi-btn hover translates the
446
+ button and deepens its block; left alone, that moves one half of a joined
447
+ control two pixels up and away from the other and tears the seam open -
448
+ the most visible way this component could fail at being one button.
449
+ So: the translate moves to the wrapper, and both halves take the hover
450
+ block whichever one the cursor is over.
451
+
452
+ What stays per-half is colour. The generic hover's dim-to-plain brighten
453
+ still lands only on the half under the cursor, which is the whole reason a
454
+ split button has two targets - the control lifts as one, and the reader can
455
+ still see which action they are about to take.
456
+
457
+ Specificity: these two rules weigh the same as `.axi-btn--primary:hover`
458
+ and win on source order, which is the ordering this section depends on -
459
+ it must stay below the button section it adjusts. The `:where()` keeps the
460
+ wrapper's hover from spending a point, so the ink layer still out-ranks
461
+ everything here, per rule 6. */
462
+ .axi-btn-split:where(:hover) { transform: translate(-2px, -2px); }
463
+ .axi-btn-split > .axi-btn:where(:hover) { transform: none; }
464
+ .axi-btn-split:where(:hover) > .axi-btn { box-shadow: var(--axi-shadow-control); }
465
+ .axi-btn-split:where(:hover) > .axi-btn--primary { box-shadow: var(--axi-shadow-control-hover); }
466
+ /* The disclosure half. Vertical padding is zero because the stretch above
467
+ already gave it the label half's height, and 20px sides around a single
468
+ chevron is the wide-rectangle failure --icon exists to fix. The gap is for
469
+ the consumers that put a status dot beside the chevron. */
470
+ .axi-btn-split__toggle { --axi-btn-pad: 0 11px; gap: 5px; }
471
+
326
472
  /* ---------- pill ---------- */
327
473
  /* A filter toggle. Its pressed state fills with whatever colour the consumer
328
474
  has put on it, so the control reads as the thing it filters to rather than
package/src/shells.css CHANGED
@@ -947,6 +947,12 @@
947
947
  display: flex; align-items: center; gap: 12px;
948
948
  height: 38px; padding-left: 14px;
949
949
  background: var(--axi-ground-deep);
950
+ /* The chrome takes the surface filter for the same reason a panel does: on a
951
+ theme that makes its surfaces translucent, the one strip a reader never
952
+ scrolls away is the one that has to agree. --axi-surface-filter is none on
953
+ every opaque surface, so this is inert there - the same inert-by-default
954
+ shape the panels already use. */
955
+ backdrop-filter: var(--axi-surface-filter);
950
956
  border-bottom: var(--axi-border-control) solid var(--axi-ink-line);
951
957
  font: var(--axi-t-micro);
952
958
  letter-spacing: var(--axi-ls-micro);
@@ -971,6 +977,13 @@
971
977
  /* Close is the one destructive control in the chrome, and the one place a
972
978
  status ink belongs in a titlebar. */
973
979
  .axi-titlebar__btns :where(button:last-child:hover) { background: var(--axi-danger); color: var(--axi-ink-on-fill); }
980
+ /* The page-wide ring sits 2px OUTSIDE its element, which a window button
981
+ cannot afford: it is flush to the top of the frame and flush to its
982
+ neighbours, so an outward ring spills past the window edge and reads as a
983
+ loud accent box across the corner of the chrome. Pull it inward, the same
984
+ way a picker option and a select option already do inside their own tight
985
+ containers. */
986
+ .axi-titlebar__btns :where(button:focus-visible) { outline-offset: -3px; }
974
987
  /* The scrolling region under the chrome. */
975
988
  .axi-window__body { flex: 1; overflow-y: auto; }
976
989