@junoput01/junoui 0.5.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,89 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 71c069c: Two primitives stop encoding junoui's own dimensions and start exposing the
8
+ derivation, so a consumer that parameterizes them stays correct.
9
+
10
+ - **`junoui/icons/install`** — the sprite injector without the sprite (~1 kB).
11
+ A consumer that subsets with `junoui/subset` can now keep the same-document
12
+ injection Safari requires without pulling the 66-symbol payload that
13
+ `junoui/icons/inline` carries; both share one id-guarded holder, so mixing
14
+ them cannot produce two. `icons/inline` is unchanged for everyone else.
15
+ - **`--juno-dock-clearance` / `--juno-pillbar-clearance` are derived**, from
16
+ new published parts: `--juno-dock-h`, `--juno-pillbar-h` and
17
+ `--juno-dock-clearance-scale`. They were constants that promised to track the
18
+ dock's geometry and did not — past a 58px bubble they reserved less than the
19
+ pill's own height plus its margin, hiding content under the dock. Values at
20
+ the default 44px bubble change from 92px to 86px (dock) and 72px to 78px
21
+ (pillbar), both now equal to what the control actually measures plus its
22
+ margin and a breathing gap.
23
+
24
+ - 0ee41a8: Declare the browser-support baseline, and guard the one gap that is functional.
25
+
26
+ New `docs/browser-support.md`: the supported floor (Safari/iOS 17.5, Chrome/Edge
27
+ 117, Firefox 129), the hard floor below which things break (17.0 / 114 / 125),
28
+ and a per-feature audit of the built bundle with a degrade-vs-break verdict on
29
+ each. `package.json` now carries a matching `browserslist`; README and
30
+ getting-started state the floor.
31
+
32
+ `base.css` ships one `@supports not selector(:popover-open)` guard. Below Safari
33
+ 17.0 the Popover API is absent, and because the UA rule that hides a closed
34
+ popover is absent with it, `.juno-menu` and `.juno-popover` were rendering as
35
+ invisible fixed panels that swallowed taps. The guard hides them instead —
36
+ absent beats invisibly-present, and apps can branch on
37
+ `CSS.supports('selector(:popover-open)')`. Guards are for functional failures
38
+ only; cosmetic gaps (missing entry animations, unanchored placement) are
39
+ documented, not wrapped.
40
+
41
+ `docs/ios-conformance.md` gains the viewport-unit decision it was missing:
42
+ `dvh` stays at both `.juno-app-shell` and `.juno-drawer`, with the reasoning,
43
+ what each option costs at the moment browser chrome retracts, and a rule for
44
+ applying the choice to a new component. It also now records the iOS Home-Screen
45
+ standalone letterbox — iOS sizes the window from the document's resting
46
+ scrollability at launch — which is why `base.css` carries the standalone unlock.
47
+
48
+ Docs and defaults only; no token or component API changed.
49
+
50
+ ### Patch Changes
51
+
52
+ - cacdb21: Fix: the 16px `.juno-input` font floor on touch never applied.
53
+
54
+ The rule lived in `base.css`'s `@media (pointer: coarse)` block, but a media
55
+ query adds no specificity — so `components/input.css`'s own
56
+ `.juno-input { font-size: var(--juno-font-size-14) }`, same 0,1,0 selector and
57
+ later in the bundle, won every time. On a coarse pointer the field measured
58
+ 14px, i.e. exactly the condition the floor exists to avoid (iOS Safari zooming
59
+ the page onto a focused sub-16px field). The rule now lives in `input.css`,
60
+ after the declaration it has to beat.
61
+
62
+ Found by the new coarse-pointer visual-regression project (20260815-006): the
63
+ suite ran only `Desktop Chrome`, where `(pointer: coarse)` never matches, so
64
+ nothing had ever exercised the rule.
65
+
66
+ - da03666: Fix: the forced-colors (Windows High Contrast) border never applied.
67
+
68
+ `@media (forced-colors: active) { .juno-badge, .juno-btn, .juno-card,
69
+ .juno-readout { border: 1px solid CanvasText } }` lived in `base.css`, which the
70
+ bundler emits before `components/`. A media query adds no specificity, so each
71
+ component's own `border` declaration won on source order alone. Measured under
72
+ emulated forced-colors: `.juno-badge` computed a transparent border — it sets
73
+ `forced-color-adjust: none` to keep its status fill, and that opt-out also
74
+ disables the UA repaint that was silently rescuing `.juno-btn` and `.juno-card`.
75
+
76
+ New `src/css/overrides.css`, bundled last, is where cross-cutting `@media` /
77
+ `@supports` gates live now — after everything they guard. Same reasoning the
78
+ bundler already applied to `utilities.css`.
79
+
80
+ - 2ec7cff: Stop publishing junoui's internal release process to consumers: the pre-release
81
+ consumer gate document moves from `docs/release-gate.md` to `RELEASING.md` at
82
+ the repo root, beside CONTRIBUTING.md, which has never shipped. `files` carries
83
+ `docs` wholesale, so the rule is now positional — `docs/` IS the published
84
+ manual, and contributor or process documents live at the root. The published
85
+ surface loses one file (204 → 203) and no consumer-facing content changes.
86
+
3
87
  ## 0.5.0
4
88
 
5
89
  ### Minor Changes
package/README.md CHANGED
@@ -61,6 +61,21 @@ build-time sRGB hex (identical rendering).
61
61
  | ---------------- | -------------------------- | ------------------------------- |
62
62
  | CSS, SCSS, JS/TS | Android (xml), iOS (Swift) | Flutter (Dart), JSON (W3C DTCG) |
63
63
 
64
+ ## Browser support
65
+
66
+ **Supported floor: Safari / iOS 17.5, Chrome / Edge 117, Firefox 129.**
67
+ Below **17.0 / 114 / 125** (the `browserslist` in `package.json`) the Popover
68
+ API is absent and the overlay surfaces — menu, popover, tooltip, and the
69
+ pillbar's overflow slot — stop working; junoui ships an `@supports` guard so
70
+ they are absent rather than invisibly blocking taps. Between the two floors
71
+ everything functions and entry animations are missing. Full fidelity, including
72
+ anchored placement for those surfaces, wants Safari 26.
73
+
74
+ CSS failures are silent — an unsupported at-rule is dropped, never reported —
75
+ so the degrade-vs-break audit, the version table and the guard rule live in
76
+ **[browser-support.md](./docs/browser-support.md)**. Read it before adding a
77
+ feature newer than the floor.
78
+
64
79
  ## Documentation
65
80
 
66
81
  | | |
@@ -73,8 +88,10 @@ build-time sRGB hex (identical rendering).
73
88
  | [Boot shell](./docs/boot-shell.md) | Fast first paint: pre-bundle shell, lazy screens, warming |
74
89
  | [Accessibility](./docs/accessibility.md) | WCAG 2.2 + WAI-ARIA references, focus, motion, targets, RTL |
75
90
  | [iOS conformance](./docs/ios-conformance.md) | Sourced iOS metrics — safe areas, tap targets, viewport units |
91
+ | [Browser support](./docs/browser-support.md) | The supported floor, what degrades below it, what breaks |
76
92
  | [Token reference](./docs/tokens-reference.md) | Every token + value + platform name (generated) |
77
93
  | [Components](./docs/components/README.md) | 30+ — forms, overlays, table, alerts, tabs, icons, nav, more |
94
+ | [Release gate](./RELEASING.md) | The consumer build every release candidate must pass |
78
95
  | [Contributing](./CONTRIBUTING.md) | Add tokens/components, lint, test, release |
79
96
  | [Roadmap](./docs/roadmap.md) | Missing capabilities, prioritised |
80
97
 
@@ -116,6 +133,18 @@ remain — publishes `@junoput01/junoui` to npm automatically. No manual `npm ru
116
133
  version` step. Bump by the token contract: remove/rename a token or class → major,
117
134
  additive → minor, fix → patch. See [CONTRIBUTING](./CONTRIBUTING.md#releasing).
118
135
 
136
+ One step is **not** automated, on purpose. Before merging the version PR, run
137
+
138
+ ```sh
139
+ npm run gate:consumer
140
+ ```
141
+
142
+ which packs the candidate, installs that tarball into a throwaway checkout of a real
143
+ consumer, and runs the consumer's typecheck, tests and production build against it. A
144
+ red gate blocks the release. It is a local step because the consumer lives on the dev
145
+ box, not in this repo's CI — [RELEASING.md](./RELEASING.md) explains
146
+ what it asserts and what it would take to move it into CI.
147
+
119
148
  ## License
120
149
 
121
150
  MIT
package/dist/css/juno.css CHANGED
@@ -370,13 +370,48 @@
370
370
  /* Floating-nav scroll clearance — a page/scroller that a fixed dock or
371
371
  pillbar floats over reserves this much room at its foot so the last row
372
372
  clears the overlay. Consumers write `padding-block-end:
373
- var(--juno-dock-clearance)` and stay correct when the dock geometry
374
- changes no hand-rolled calc() per app. Web-only (they carry env()), so
375
- they live in the CSS layer, not the cross-platform token set.
376
- --juno-dock-clearance pill height + bottom margin + breathing room;
377
- --juno-pillbar-clearance suits the shorter centered pill. */
378
- --juno-dock-clearance: calc(var(--juno-space-72) + var(--juno-space-20) + env(safe-area-inset-bottom, 0px));
379
- --juno-pillbar-clearance: calc(var(--juno-space-72) + env(safe-area-inset-bottom, 0px));
373
+ var(--juno-dock-clearance)`. Web-only (they carry env()), so they live in
374
+ the CSS layer, not the cross-platform token set.
375
+
376
+ DERIVED, not constant. These used to be `space-72 + space-20 + env()`,
377
+ which promised in this very comment to "stay correct when the dock
378
+ geometry changes" and could not: the floating pill measures 62px with the
379
+ default 44px bubble, and junoui explicitly invites a consumer to
380
+ parameterize that bubble (--juno-size-tap-comfortable). Past a 58px bubble
381
+ the old constant reserved LESS than the pill's own height plus its margin
382
+ — content hid under the dock, silently, on exactly the consumers who took
383
+ the invitation (20260815-026).
384
+
385
+ So the parts are named and the arithmetic is visible:
386
+ --juno-dock-h the pill's laid-out height: the bubble, the item's
387
+ padding-block, the pill's own padding, its border.
388
+ * -clearance-scale consumers that SHRINK the bar while scrolling
389
+ reserve at the small size, not the live one — a
390
+ reservation that tracks a value which changes after
391
+ the last scroll event relayouts content under a
392
+ finger that has already stopped moving. Default 1.
393
+ + space-16 + env() the pill's own bottom margin (dock.css), additive
394
+ because a floating element sits OFF the edge —
395
+ docs/ios-conformance.md, "max() vs addition".
396
+ + space-8 breathing room, so the last row is not flush. */
397
+ --juno-dock-h: calc(
398
+ var(--juno-size-tap-comfortable) + 4 * var(--juno-space-4) + 2 * var(--juno-border-width-1)
399
+ );
400
+ --juno-dock-clearance-scale: 1;
401
+ --juno-dock-clearance: calc(
402
+ var(--juno-dock-h) * var(--juno-dock-clearance-scale) + var(--juno-space-16) +
403
+ var(--juno-space-8) + env(safe-area-inset-bottom, 0px)
404
+ );
405
+
406
+ /* The pillbar is the same shape one size down: no bubble, so its height is
407
+ the tap target plus its own padding and border. */
408
+ --juno-pillbar-h: calc(
409
+ var(--juno-size-tap-comfortable) + 2 * var(--juno-space-4) + 2 * var(--juno-border-width-1)
410
+ );
411
+ --juno-pillbar-clearance: calc(
412
+ var(--juno-pillbar-h) * var(--juno-dock-clearance-scale) + var(--juno-space-16) +
413
+ var(--juno-space-8) + env(safe-area-inset-bottom, 0px)
414
+ );
380
415
 
381
416
  /* JS-readable motion contract. CSS-only `prefers-reduced-motion` never reaches
382
417
  imperative JS (scrollTo/scrollIntoView smooth behavior, rAF-driven transforms,
@@ -478,8 +513,11 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
478
513
  is harmless either way: hold text-entry controls (.juno-input covers input,
479
514
  textarea and the select's inner control) at a 16px floor on touch. max()
480
515
  keeps the floor even under a scaled-down --juno-font-scale, and still grows
481
- when scaled up. */
482
- .juno-input { font-size: max(16px, var(--juno-font-size-16)); }
516
+ when scaled up.
517
+ THE RULE ITSELF LIVES IN components/input.css, not here: a media query adds
518
+ no specificity, so this file's `.juno-input` would lose to input.css's own
519
+ `.juno-input` font-size later in the bundle. It did, silently, until
520
+ 20260815-006's coarse-pointer project measured it. */
483
521
 
484
522
  /* Kill the UA tap-highlight square on the interactive surfaces so it never
485
523
  flashes past a rounded control on tap. Consumers were adding this by hand
@@ -580,17 +618,11 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
580
618
  :root { --juno-border: var(--juno-border-strong); }
581
619
  }
582
620
 
583
- /* Windows High Contrast / forced-colors: opt into the system palette so
584
- borders and focus stay visible when our colors are overridden. */
585
- @media (forced-colors: active) {
586
- *:focus-visible { outline-color: Highlight; }
587
-
588
- .juno-badge,
589
- .juno-btn,
590
- .juno-card,
591
- .juno-readout { border: 1px solid CanvasText; }
592
- .juno-badge { forced-color-adjust: none; } /* keep status fill meaningful */
593
- }
621
+ /* Windows High Contrast / forced-colors: the opt-in lives in
622
+ overrides.css, NOT here. A media query adds no specificity, and this file is
623
+ bundled BEFORE components/, so `.juno-btn { border: }` in button.css beat
624
+ the forced-colors border every time — same 0,1,0 selector, later in the
625
+ bundle. See src/css/overrides.css and ticket 20260815-029. */
594
626
 
595
627
  /* ── iOS standalone letterbox unlock ─────────────────────────────────────────
596
628
  iOS WebKit (observed through 26.6, iPhone 16 Pro, iOS 18.7) sizes a
@@ -640,6 +672,33 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
640
672
  }
641
673
  }
642
674
 
675
+ /* ── Popover API absent (Safari/iOS < 17.0) ──────────────────────────────────
676
+ junoui's overlay surfaces are built on the native Popover API. Without it
677
+ `popovertarget` does nothing, so they cannot open — an inconvenience. The
678
+ part that is a BUG is that the UA rule which hides a closed popover
679
+ (`[popover]:not(:popover-open) { display: none }`) does not exist either, so
680
+ the panel participates in layout: .juno-menu and .juno-popover are
681
+ `position: fixed` with `opacity: 0` and no pointer-events reset, i.e. a
682
+ 256-280px INVISIBLE panel parked at its static position, swallowing taps on
683
+ whatever it covers. (.juno-tooltip__bubble escapes it — it inherits
684
+ `pointer-events: none` from the CSS-only tooltip rule — and is listed anyway
685
+ so the set matches the docs.)
686
+
687
+ So: hide them. Absent beats invisibly-present. Apps detect the same condition
688
+ with CSS.supports('selector(:popover-open)') and render a fallback.
689
+
690
+ @supports selector() is itself Safari 14.1+; below that this condition is
691
+ invalid, the block is skipped, and behaviour is what it is today. Fail-open,
692
+ and far below any supported floor. Guards live here rather than per component
693
+ so the next popover-based component inherits it — see docs/browser-support.md
694
+ for the rule about when a feature gap earns a guard at all (functional
695
+ failures only; a missing animation does not get one). */
696
+ @supports not selector(:popover-open) {
697
+ .juno-menu[popover],
698
+ .juno-popover[popover],
699
+ .juno-tooltip__bubble[popover] { display: none; }
700
+ }
701
+
643
702
  /* ════════════════════════════════════════════════════════════════════
644
703
  * junoui — layout layer (intrinsic & responsive)
645
704
  *
@@ -2522,6 +2581,18 @@ button.juno-chip {
2522
2581
  color: var(--juno-data);
2523
2582
  }
2524
2583
 
2584
+ /* The iOS focus-zoom floor, and it has to live HERE, not in base.css's
2585
+ `@media (pointer: coarse)` block where it was written. A media query adds no
2586
+ specificity, so `.juno-input { font-size: var(--juno-font-size-14) }` above —
2587
+ same 0,1,0 selector, later in the bundle (base.css sorts before components/)
2588
+ — simply won it, and the floor never applied on any touch device. Nothing
2589
+ caught it because the suite had no coarse-pointer project until 20260815-006
2590
+ added one; the numeric assertion in test/visual/tap-targets.spec.mjs is what
2591
+ surfaced it. See base.css for the sourcing caveat on the behavior itself. */
2592
+ @media (pointer: coarse) {
2593
+ .juno-input { font-size: max(16px, var(--juno-font-size-16)); }
2594
+ }
2595
+
2525
2596
  .juno-input--sans { font-family: var(--juno-font-family-sans); }
2526
2597
 
2527
2598
  .juno-input::placeholder { color: var(--juno-muted); }
@@ -3734,7 +3805,7 @@ button.juno-list__row:disabled {
3734
3805
  <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-dots-three" /></svg>
3735
3806
  </button>
3736
3807
  <div class="juno-pillbar__tray">
3737
- <div><!-- single wrapper, any element -->
3808
+ <div>\3c !-- single wrapper, any element -->
3738
3809
  …the usual __item / __sep / __input children…
3739
3810
  </div>
3740
3811
  </div>
@@ -5794,3 +5865,55 @@ button.juno-seg__opt:disabled {
5794
5865
  white-space: nowrap;
5795
5866
  border: 0;
5796
5867
  }
5868
+
5869
+ /* ════════════════════════════════════════════════════════════════════
5870
+ * Overrides — cross-cutting gates, bundled LAST
5871
+ * ════════════════════════════════════════════════════════════════════
5872
+ * A @media or @supports block adds NO specificity. So a gate written in
5873
+ * base.css — which the bundler emits before components/ — loses to any
5874
+ * component rule declaring the same property on the same selector, purely
5875
+ * on source order, and loses SILENTLY: the gate looks right in the file it
5876
+ * was written in, and there is no error anywhere.
5877
+ *
5878
+ * Four instances of that shipped before this file existed (2026-08-15):
5879
+ * - .juno-input's 16px iOS focus-zoom floor, beaten by input.css. The floor
5880
+ * never applied on any touch device, in any release (20260815-011).
5881
+ * - the forced-colors border below, beaten by button.css / badge.css /
5882
+ * card.css (20260815-029).
5883
+ * - the Popover fallback, which happens to win today only because nothing
5884
+ * later declares `display` on those selectors — pinned by a test rather
5885
+ * than by structure (20260815-013).
5886
+ *
5887
+ * So: gates whose whole job is to BEAT a component default live here, after
5888
+ * everything they guard. Same reasoning the bundler already applies to
5889
+ * utilities.css (role helpers must outrank component defaults) — see
5890
+ * scripts/bundle-css.mjs.
5891
+ *
5892
+ * What does NOT belong here: a gate that only has to beat the UA or another
5893
+ * rule in its own file. Keep those next to what they modify; this file is for
5894
+ * the cross-file case, or it becomes a dumping ground and the cascade gets
5895
+ * harder to read rather than easier.
5896
+ *
5897
+ * A build test (test/build.test.mjs) asserts the bundle contains no gated
5898
+ * declaration that a later ungated rule overrides at equal-or-lower
5899
+ * specificity. It asserts an EMPTY SET — no allowlist, because an allowlist
5900
+ * is where violations go to be forgotten.
5901
+ * ════════════════════════════════════════════════════════════════════ */
5902
+
5903
+ /* Windows High Contrast / forced-colors: opt into the system palette so
5904
+ borders and focus stay visible when the user's palette replaces ours.
5905
+
5906
+ .juno-badge additionally sets `forced-color-adjust: none` to keep its status
5907
+ fill meaningful — and that opt-out is why the badge was the one element where
5908
+ the lost cascade actually reached the screen: it also disables the UA's own
5909
+ repaint, which was silently rescuing .juno-btn and .juno-card. Measured, not
5910
+ inferred (20260815-029). */
5911
+ @media (forced-colors: active) {
5912
+ *:focus-visible { outline-color: Highlight; }
5913
+
5914
+ .juno-badge,
5915
+ .juno-btn,
5916
+ .juno-card,
5917
+ .juno-readout { border: 1px solid CanvasText; }
5918
+ .juno-badge { forced-color-adjust: none; } /* keep status fill meaningful */
5919
+ }
@@ -1,19 +1,17 @@
1
- // junoui — inline icon sprite injector. Generated; do not edit.
1
+ // junoui — inline icon sprite injector (full set). Generated; do not edit.
2
2
  // Fixes Safari dropping external <use href="file.svg#id"> refs: this injects
3
3
  // the sprite into the document so you reference icons same-document instead:
4
4
  // import '@junoput01/junoui/icons/inline';
5
5
  // <svg class="juno-icon"><use href="#juno-i-gear" /></svg>
6
+ // Shipping a SUBSET instead? Import junoui/icons/install and pass it, so this
7
+ // module's 25 kB of symbols never enters your bundle.
8
+ import { installSprite } from './install.js';
9
+
6
10
  const SPRITE = "<svg xmlns=\"http://www.w3.org/2000/svg\" style=\"display:none\" fill=\"currentColor\">\n <symbol id=\"juno-i-arrow-clockwise\" viewBox=\"0 0 256 256\"><path d=\"M244,56v48a12,12,0,0,1-12,12H184a12,12,0,1,1,0-24H201.1l-19-17.38c-.13-.12-.26-.24-.38-.37A76,76,0,1,0,127,204h1a75.53,75.53,0,0,0,52.15-20.72,12,12,0,0,1,16.49,17.45A99.45,99.45,0,0,1,128,228h-1.37A100,100,0,1,1,198.51,57.06L220,76.72V56a12,12,0,0,1,24,0Z\"/></symbol>\n <symbol id=\"juno-i-arrow-down\" viewBox=\"0 0 256 256\"><path d=\"M208.49,152.49l-72,72a12,12,0,0,1-17,0l-72-72a12,12,0,0,1,17-17L116,187V40a12,12,0,0,1,24,0V187l51.51-51.52a12,12,0,0,1,17,17Z\"/></symbol>\n <symbol id=\"juno-i-arrow-left\" viewBox=\"0 0 256 256\"><path d=\"M228,128a12,12,0,0,1-12,12H69l51.52,51.51a12,12,0,0,1-17,17l-72-72a12,12,0,0,1,0-17l72-72a12,12,0,0,1,17,17L69,116H216A12,12,0,0,1,228,128Z\"/></symbol>\n <symbol id=\"juno-i-arrow-right\" viewBox=\"0 0 256 256\"><path d=\"M224.49,136.49l-72,72a12,12,0,0,1-17-17L187,140H40a12,12,0,0,1,0-24H187L135.51,64.48a12,12,0,0,1,17-17l72,72A12,12,0,0,1,224.49,136.49Z\"/></symbol>\n <symbol id=\"juno-i-arrow-up\" viewBox=\"0 0 256 256\"><path d=\"M208.49,120.49a12,12,0,0,1-17,0L140,69V216a12,12,0,0,1-24,0V69L64.49,120.49a12,12,0,0,1-17-17l72-72a12,12,0,0,1,17,0l72,72A12,12,0,0,1,208.49,120.49Z\"/></symbol>\n <symbol id=\"juno-i-arrows-clockwise\" viewBox=\"0 0 256 256\"><path d=\"M228,48V96a12,12,0,0,1-12,12H168a12,12,0,0,1,0-24h19l-7.8-7.8a75.55,75.55,0,0,0-53.32-22.26h-.43A75.49,75.49,0,0,0,72.39,75.57,12,12,0,1,1,55.61,58.41a99.38,99.38,0,0,1,69.87-28.47H126A99.42,99.42,0,0,1,196.2,59.23L204,67V48a12,12,0,0,1,24,0ZM183.61,180.43a75.49,75.49,0,0,1-53.09,21.63h-.43A75.55,75.55,0,0,1,76.77,179.8L69,172H88a12,12,0,0,0,0-24H40a12,12,0,0,0-12,12v48a12,12,0,0,0,24,0V189l7.8,7.8A99.42,99.42,0,0,0,130,226.06h.56a99.38,99.38,0,0,0,69.87-28.47,12,12,0,0,0-16.78-17.16Z\"/></symbol>\n <symbol id=\"juno-i-arrows-out\" viewBox=\"0 0 256 256\"><path d=\"M220,48V96a12,12,0,0,1-24,0V77l-35.51,35.52a12,12,0,0,1-17-17L179,60H160a12,12,0,0,1,0-24h48A12,12,0,0,1,220,48ZM95.51,143.51,60,179V160a12,12,0,0,0-24,0v48a12,12,0,0,0,12,12H96a12,12,0,0,0,0-24H77l35.52-35.51a12,12,0,0,0-17-17ZM208,148a12,12,0,0,0-12,12v19l-35.51-35.52a12,12,0,0,0-17,17L179,196H160a12,12,0,0,0,0,24h48a12,12,0,0,0,12-12V160A12,12,0,0,0,208,148ZM77,60H96a12,12,0,0,0,0-24H48A12,12,0,0,0,36,48V96a12,12,0,0,0,24,0V77l35.51,35.52a12,12,0,0,0,17-17Z\"/></symbol>\n <symbol id=\"juno-i-bell\" viewBox=\"0 0 256 256\"><path d=\"M225.29,165.93C216.61,151,212,129.57,212,104a84,84,0,0,0-168,0c0,25.58-4.59,47-13.27,61.93A20.08,20.08,0,0,0,30.66,186,19.77,19.77,0,0,0,48,196H84.18a44,44,0,0,0,87.64,0H208a19.77,19.77,0,0,0,17.31-10A20.08,20.08,0,0,0,225.29,165.93ZM128,212a20,20,0,0,1-19.6-16h39.2A20,20,0,0,1,128,212ZM54.66,172C63.51,154,68,131.14,68,104a60,60,0,0,1,120,0c0,27.13,4.48,50,13.33,68Z\"/></symbol>\n <symbol id=\"juno-i-calendar-blank\" viewBox=\"0 0 256 256\"><path d=\"M208,28H188V24a12,12,0,0,0-24,0v4H92V24a12,12,0,0,0-24,0v4H48A20,20,0,0,0,28,48V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V48A20,20,0,0,0,208,28ZM68,52a12,12,0,0,0,24,0h72a12,12,0,0,0,24,0h16V76H52V52ZM52,204V100H204V204Z\"/></symbol>\n <symbol id=\"juno-i-caret-down\" viewBox=\"0 0 256 256\"><path d=\"M216.49,104.49l-80,80a12,12,0,0,1-17,0l-80-80a12,12,0,0,1,17-17L128,159l71.51-71.52a12,12,0,0,1,17,17Z\"/></symbol>\n <symbol id=\"juno-i-caret-left\" viewBox=\"0 0 256 256\"><path d=\"M168.49,199.51a12,12,0,0,1-17,17l-80-80a12,12,0,0,1,0-17l80-80a12,12,0,0,1,17,17L97,128Z\"/></symbol>\n <symbol id=\"juno-i-caret-right\" viewBox=\"0 0 256 256\"><path d=\"M184.49,136.49l-80,80a12,12,0,0,1-17-17L159,128,87.51,56.49a12,12,0,1,1,17-17l80,80A12,12,0,0,1,184.49,136.49Z\"/></symbol>\n <symbol id=\"juno-i-caret-up\" viewBox=\"0 0 256 256\"><path d=\"M216.49,168.49a12,12,0,0,1-17,0L128,97,56.49,168.49a12,12,0,0,1-17-17l80-80a12,12,0,0,1,17,0l80,80A12,12,0,0,1,216.49,168.49Z\"/></symbol>\n <symbol id=\"juno-i-chat-circle\" viewBox=\"0 0 256 256\"><path d=\"M128,20A108,108,0,0,0,31.85,177.23L21,209.66A20,20,0,0,0,46.34,235l32.43-10.81A108,108,0,1,0,128,20Zm0,192a84,84,0,0,1-42.06-11.27,12,12,0,0,0-6-1.62,12.1,12.1,0,0,0-3.8.62l-29.79,9.93,9.93-29.79a12,12,0,0,0-1-9.81A84,84,0,1,1,128,212Z\"/></symbol>\n <symbol id=\"juno-i-check-circle\" viewBox=\"0 0 256 256\"><path d=\"M176.49,95.51a12,12,0,0,1,0,17l-56,56a12,12,0,0,1-17,0l-24-24a12,12,0,1,1,17-17L112,143l47.51-47.52A12,12,0,0,1,176.49,95.51ZM236,128A108,108,0,1,1,128,20,108.12,108.12,0,0,1,236,128Zm-24,0a84,84,0,1,0-84,84A84.09,84.09,0,0,0,212,128Z\"/></symbol>\n <symbol id=\"juno-i-check\" viewBox=\"0 0 256 256\"><path d=\"M232.49,80.49l-128,128a12,12,0,0,1-17,0l-56-56a12,12,0,1,1,17-17L96,183,215.51,63.51a12,12,0,0,1,17,17Z\"/></symbol>\n <symbol id=\"juno-i-clock\" viewBox=\"0 0 256 256\"><path d=\"M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212Zm68-84a12,12,0,0,1-12,12H128a12,12,0,0,1-12-12V72a12,12,0,0,1,24,0v44h44A12,12,0,0,1,196,128Z\"/></symbol>\n <symbol id=\"juno-i-cloud-arrow-down\" viewBox=\"0 0 256 256\"><path d=\"M192.49,167.51a12,12,0,0,1,0,17l-32,32a12,12,0,0,1-17,0l-32-32a12,12,0,1,1,17-17L140,179V128a12,12,0,0,1,24,0v51l11.51-11.52A12,12,0,0,1,192.49,167.51ZM160,36A92.08,92.08,0,0,0,79,84.37,68,68,0,1,0,72,220H84a12,12,0,0,0,0-24H72a44,44,0,0,1-1.81-87.95A91.7,91.7,0,0,0,68,128a12,12,0,0,0,24,0,68,68,0,0,1,136,0,67.27,67.27,0,0,1-7.25,30.59,12,12,0,1,0,21.42,10.82A91.08,91.08,0,0,0,252,128,92.1,92.1,0,0,0,160,36Z\"/></symbol>\n <symbol id=\"juno-i-cloud-slash\" viewBox=\"0 0 256 256\"><path d=\"M56.88,31.93A12,12,0,1,0,39.12,48.07L71.79,84A68,68,0,0,0,72,220h88a91.26,91.26,0,0,0,30.66-5.24l8.46,9.31a12,12,0,0,0,17.76-16.14ZM160,196H72a44,44,0,0,1-1.8-87.95A91.91,91.91,0,0,0,68,128a12,12,0,0,0,24,0,68.22,68.22,0,0,1,2.66-18.84l77.88,85.67A68.67,68.67,0,0,1,160,196Zm92-68a91.32,91.32,0,0,1-17.53,54,12,12,0,1,1-19.41-14.11,68,68,0,0,0-89.57-98.53,12,12,0,0,1-12.2-20.66A92,92,0,0,1,252,128Z\"/></symbol>\n <symbol id=\"juno-i-cloud\" viewBox=\"0 0 256 256\"><path d=\"M160,36A92.09,92.09,0,0,0,79,84.36,68,68,0,1,0,72,220h88a92,92,0,0,0,0-184Zm0,160H72a44,44,0,0,1-1.82-88A91.86,91.86,0,0,0,68,128a12,12,0,0,0,24,0,68,68,0,1,1,68,68Z\"/></symbol>\n <symbol id=\"juno-i-copy\" viewBox=\"0 0 256 256\"><path d=\"M216,28H88A12,12,0,0,0,76,40V76H40A12,12,0,0,0,28,88V216a12,12,0,0,0,12,12H168a12,12,0,0,0,12-12V180h36a12,12,0,0,0,12-12V40A12,12,0,0,0,216,28ZM156,204H52V100H156Zm48-48H180V88a12,12,0,0,0-12-12H100V52H204Z\"/></symbol>\n <symbol id=\"juno-i-cpu\" viewBox=\"0 0 256 256\"><path d=\"M156,88H100a12,12,0,0,0-12,12v56a12,12,0,0,0,12,12h56a12,12,0,0,0,12-12V100A12,12,0,0,0,156,88Zm-12,56H112V112h32Zm88-4H220V116h12a12,12,0,0,0,0-24H220V56a20,20,0,0,0-20-20H164V24a12,12,0,0,0-24,0V36H116V24a12,12,0,0,0-24,0V36H56A20,20,0,0,0,36,56V92H24a12,12,0,0,0,0,24H36v24H24a12,12,0,0,0,0,24H36v36a20,20,0,0,0,20,20H92v12a12,12,0,0,0,24,0V220h24v12a12,12,0,0,0,24,0V220h36a20,20,0,0,0,20-20V164h12a12,12,0,0,0,0-24Zm-36,56H60V60H196Z\"/></symbol>\n <symbol id=\"juno-i-dots-three-vertical\" viewBox=\"0 0 256 256\"><path d=\"M112,60a16,16,0,1,1,16,16A16,16,0,0,1,112,60Zm16,52a16,16,0,1,0,16,16A16,16,0,0,0,128,112Zm0,68a16,16,0,1,0,16,16A16,16,0,0,0,128,180Z\"/></symbol>\n <symbol id=\"juno-i-dots-three\" viewBox=\"0 0 256 256\"><path d=\"M144,128a16,16,0,1,1-16-16A16,16,0,0,1,144,128ZM60,112a16,16,0,1,0,16,16A16,16,0,0,0,60,112Zm136,0a16,16,0,1,0,16,16A16,16,0,0,0,196,112Z\"/></symbol>\n <symbol id=\"juno-i-download\" viewBox=\"0 0 256 256\"><path d=\"M71.51,88.49a12,12,0,0,1,17-17L116,99V24a12,12,0,0,1,24,0V99l27.51-27.52a12,12,0,0,1,17,17l-48,48a12,12,0,0,1-17,0ZM224,116H188a12,12,0,0,0,0,24h32v56H36V140H68a12,12,0,0,0,0-24H32a20,20,0,0,0-20,20v64a20,20,0,0,0,20,20H224a20,20,0,0,0,20-20V136A20,20,0,0,0,224,116Zm-20,52a16,16,0,1,0-16,16A16,16,0,0,0,204,168Z\"/></symbol>\n <symbol id=\"juno-i-envelope\" viewBox=\"0 0 256 256\"><path d=\"M224,44H32A12,12,0,0,0,20,56V192a20,20,0,0,0,20,20H216a20,20,0,0,0,20-20V56A12,12,0,0,0,224,44Zm-96,83.72L62.85,68h130.3ZM92.79,128,44,172.72V83.28Zm17.76,16.28,9.34,8.57a12,12,0,0,0,16.22,0l9.34-8.57L193.15,188H62.85ZM163.21,128,212,83.28v89.44Z\"/></symbol>\n <symbol id=\"juno-i-eye-slash\" viewBox=\"0 0 256 256\"><path d=\"M56.88,31.93A12,12,0,1,0,39.12,48.07l16,17.65C20.67,88.66,5.72,121.58,5,123.13a12.08,12.08,0,0,0,0,9.75c.37.82,9.13,20.26,28.49,39.61C59.37,198.34,92,212,128,212a131.34,131.34,0,0,0,51-10l20.09,22.1a12,12,0,0,0,17.76-16.14ZM128,188c-29.59,0-55.47-10.73-76.91-31.88A130.69,130.69,0,0,1,29.52,128c5.27-9.31,18.79-29.9,42-44.29l90.09,99.11A109.33,109.33,0,0,1,128,188Zm123-55.12c-.36.81-9,20-28,39.16a12,12,0,1,1-17-16.9A130.48,130.48,0,0,0,226.48,128a130.36,130.36,0,0,0-21.57-28.12C183.46,78.73,157.59,68,128,68c-3.35,0-6.7.14-10,.42a12,12,0,1,1-2-23.91c3.93-.34,8-.51,12-.51,36,0,68.63,13.67,94.49,39.52,19.35,19.35,28.11,38.8,28.48,39.61A12.08,12.08,0,0,1,251,132.88Z\"/></symbol>\n <symbol id=\"juno-i-eye\" viewBox=\"0 0 256 256\"><path d=\"M251,123.13c-.37-.81-9.13-20.26-28.48-39.61C196.63,57.67,164,44,128,44S59.37,57.67,33.51,83.52C14.16,102.87,5.4,122.32,5,123.13a12.08,12.08,0,0,0,0,9.75c.37.82,9.13,20.26,28.49,39.61C59.37,198.34,92,212,128,212s68.63-13.66,94.48-39.51c19.36-19.35,28.12-38.79,28.49-39.61A12.08,12.08,0,0,0,251,123.13Zm-46.06,33C183.47,177.27,157.59,188,128,188s-55.47-10.73-76.91-31.88A130.36,130.36,0,0,1,29.52,128,130.45,130.45,0,0,1,51.09,99.89C72.54,78.73,98.41,68,128,68s55.46,10.73,76.91,31.89A130.36,130.36,0,0,1,226.48,128,130.45,130.45,0,0,1,204.91,156.12ZM128,84a44,44,0,1,0,44,44A44.05,44.05,0,0,0,128,84Zm0,64a20,20,0,1,1,20-20A20,20,0,0,1,128,148Z\"/></symbol>\n <symbol id=\"juno-i-file\" viewBox=\"0 0 256 256\"><path d=\"M216.49,79.52l-56-56A12,12,0,0,0,152,20H56A20,20,0,0,0,36,40V216a20,20,0,0,0,20,20H200a20,20,0,0,0,20-20V88A12,12,0,0,0,216.49,79.52ZM160,57l23,23H160ZM60,212V44h76V92a12,12,0,0,0,12,12h48V212Z\"/></symbol>\n <symbol id=\"juno-i-film-strip\" viewBox=\"0 0 256 256\"><path d=\"M216,36H40A20,20,0,0,0,20,56V200a20,20,0,0,0,20,20H216a20,20,0,0,0,20-20V56A20,20,0,0,0,216,36ZM44,100h72v56H44Zm96-24V60h24V76Zm-24,0H92V60h24Zm0,104v16H92V180Zm24,0h24v16H140Zm0-24V100h72v56Zm72-80H188V60h24ZM68,60V76H44V60ZM44,180H68v16H44Zm144,16V180h24v16Z\"/></symbol>\n <symbol id=\"juno-i-floppy-disk\" viewBox=\"0 0 256 256\"><path d=\"M222.14,69.17,186.83,33.86A19.86,19.86,0,0,0,172.69,28H48A20,20,0,0,0,28,48V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V83.31A19.86,19.86,0,0,0,222.14,69.17ZM164,204H92V160h72Zm40,0H188V156a20,20,0,0,0-20-20H88a20,20,0,0,0-20,20v48H52V52H171l33,33ZM164,84a12,12,0,0,1-12,12H96a12,12,0,0,1,0-24h56A12,12,0,0,1,164,84Z\"/></symbol>\n <symbol id=\"juno-i-folder\" viewBox=\"0 0 256 256\"><path d=\"M216,68H133.39l-26-29.29a20,20,0,0,0-15-6.71H40A20,20,0,0,0,20,52V200.62A19.41,19.41,0,0,0,39.38,220H216.89A19.13,19.13,0,0,0,236,200.89V88A20,20,0,0,0,216,68ZM44,56H90.61l10.67,12H44ZM212,196H44V92H212Z\"/></symbol>\n <symbol id=\"juno-i-funnel\" viewBox=\"0 0 256 256\"><path d=\"M234.29,47.91A20,20,0,0,0,216,36H40A20,20,0,0,0,25.2,69.45l.12.14L92,140.75V216a20,20,0,0,0,31.1,16.64l32-21.33A20,20,0,0,0,164,194.66V140.75l66.67-71.16.12-.14A20,20,0,0,0,234.29,47.91Zm-91,79.89A12,12,0,0,0,140,136v56.52l-24,16V136a12,12,0,0,0-3.25-8.2L49.23,60H206.77Z\"/></symbol>\n <symbol id=\"juno-i-gear\" viewBox=\"0 0 256 256\"><path d=\"M128,76a52,52,0,1,0,52,52A52.06,52.06,0,0,0,128,76Zm0,80a28,28,0,1,1,28-28A28,28,0,0,1,128,156Zm92-27.21v-1.58l14-17.51a12,12,0,0,0,2.23-10.59A111.75,111.75,0,0,0,225,71.89,12,12,0,0,0,215.89,66L193.61,63.5l-1.11-1.11L190,40.1A12,12,0,0,0,184.11,31a111.67,111.67,0,0,0-27.23-11.27A12,12,0,0,0,146.3,22L128.79,36h-1.58L109.7,22a12,12,0,0,0-10.59-2.23A111.75,111.75,0,0,0,71.89,31.05,12,12,0,0,0,66,40.11L63.5,62.39,62.39,63.5,40.1,66A12,12,0,0,0,31,71.89,111.67,111.67,0,0,0,19.77,99.12,12,12,0,0,0,22,109.7l14,17.51v1.58L22,146.3a12,12,0,0,0-2.23,10.59,111.75,111.75,0,0,0,11.29,27.22A12,12,0,0,0,40.11,190l22.28,2.48,1.11,1.11L66,215.9A12,12,0,0,0,71.89,225a111.67,111.67,0,0,0,27.23,11.27A12,12,0,0,0,109.7,234l17.51-14h1.58l17.51,14a12,12,0,0,0,10.59,2.23A111.75,111.75,0,0,0,184.11,225a12,12,0,0,0,5.91-9.06l2.48-22.28,1.11-1.11L215.9,190a12,12,0,0,0,9.06-5.91,111.67,111.67,0,0,0,11.27-27.23A12,12,0,0,0,234,146.3Zm-24.12-4.89a70.1,70.1,0,0,1,0,8.2,12,12,0,0,0,2.61,8.22l12.84,16.05A86.47,86.47,0,0,1,207,166.86l-20.43,2.27a12,12,0,0,0-7.65,4,69,69,0,0,1-5.8,5.8,12,12,0,0,0-4,7.65L166.86,207a86.47,86.47,0,0,1-10.49,4.35l-16.05-12.85a12,12,0,0,0-7.5-2.62c-.24,0-.48,0-.72,0a70.1,70.1,0,0,1-8.2,0,12.06,12.06,0,0,0-8.22,2.6L99.63,211.33A86.47,86.47,0,0,1,89.14,207l-2.27-20.43a12,12,0,0,0-4-7.65,69,69,0,0,1-5.8-5.8,12,12,0,0,0-7.65-4L49,166.86a86.47,86.47,0,0,1-4.35-10.49l12.84-16.05a12,12,0,0,0,2.61-8.22,70.1,70.1,0,0,1,0-8.2,12,12,0,0,0-2.61-8.22L44.67,99.63A86.47,86.47,0,0,1,49,89.14l20.43-2.27a12,12,0,0,0,7.65-4,69,69,0,0,1,5.8-5.8,12,12,0,0,0,4-7.65L89.14,49a86.47,86.47,0,0,1,10.49-4.35l16.05,12.85a12.06,12.06,0,0,0,8.22,2.6,70.1,70.1,0,0,1,8.2,0,12,12,0,0,0,8.22-2.6l16.05-12.85A86.47,86.47,0,0,1,166.86,49l2.27,20.43a12,12,0,0,0,4,7.65,69,69,0,0,1,5.8,5.8,12,12,0,0,0,7.65,4L207,89.14a86.47,86.47,0,0,1,4.35,10.49l-12.84,16.05A12,12,0,0,0,195.88,123.9Z\"/></symbol>\n <symbol id=\"juno-i-hard-drives\" viewBox=\"0 0 256 256\"><path d=\"M208,36H48A20,20,0,0,0,28,56V200a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V56A20,20,0,0,0,208,36Zm-4,24v56H52V60ZM52,196V140H204v56ZM160,88a16,16,0,1,1,16,16A16,16,0,0,1,160,88Zm32,80a16,16,0,1,1-16-16A16,16,0,0,1,192,168Z\"/></symbol>\n <symbol id=\"juno-i-heart\" viewBox=\"0 0 256 256\"><path d=\"M178,36c-20.09,0-37.92,7.93-50,21.56C115.92,43.93,98.09,36,78,36a66.08,66.08,0,0,0-66,66c0,72.34,105.81,130.14,110.31,132.57a12,12,0,0,0,11.38,0C138.19,232.14,244,174.34,244,102A66.08,66.08,0,0,0,178,36Zm-5.49,142.36A328.69,328.69,0,0,1,128,210.16a328.69,328.69,0,0,1-44.51-31.8C61.82,159.77,36,131.42,36,102A42,42,0,0,1,78,60c17.8,0,32.7,9.4,38.89,24.54a12,12,0,0,0,22.22,0C145.3,69.4,160.2,60,178,60a42,42,0,0,1,42,42C220,131.42,194.18,159.77,172.51,178.36Z\"/></symbol>\n <symbol id=\"juno-i-hexagon\" viewBox=\"0 0 256 256\"><path d=\"M225.6,62.64l-88-48.17a19.91,19.91,0,0,0-19.2,0l-88,48.17A20,20,0,0,0,20,80.19v95.62a20,20,0,0,0,10.4,17.55l88,48.17a19.89,19.89,0,0,0,19.2,0l88-48.17A20,20,0,0,0,236,175.81V80.19A20,20,0,0,0,225.6,62.64ZM212,173.44l-84,46-84-46V82.56l84-46,84,46Z\"/></symbol>\n <symbol id=\"juno-i-house\" viewBox=\"0 0 256 256\"><path d=\"M222.14,105.85l-80-80a20,20,0,0,0-28.28,0l-80,80A19.86,19.86,0,0,0,28,120v96a12,12,0,0,0,12,12h64a12,12,0,0,0,12-12V164h24v52a12,12,0,0,0,12,12h64a12,12,0,0,0,12-12V120A19.86,19.86,0,0,0,222.14,105.85ZM204,204H164V152a12,12,0,0,0-12-12H104a12,12,0,0,0-12,12v52H52V121.65l76-76,76,76Z\"/></symbol>\n <symbol id=\"juno-i-images\" viewBox=\"0 0 256 256\"><path d=\"M160,88a16,16,0,1,1,16,16A16,16,0,0,1,160,88Zm76-32V160a20,20,0,0,1-20,20H204v20a20,20,0,0,1-20,20H40a20,20,0,0,1-20-20V88A20,20,0,0,1,40,68H60V56A20,20,0,0,1,80,36H216A20,20,0,0,1,236,56ZM180,180H80a20,20,0,0,1-20-20V92H44V196H180Zm-21.66-24L124,121.66,89.66,156ZM212,60H84v67.72l25.86-25.86a20,20,0,0,1,28.28,0L192.28,156H212Z\"/></symbol>\n <symbol id=\"juno-i-info\" viewBox=\"0 0 256 256\"><path d=\"M108,84a16,16,0,1,1,16,16A16,16,0,0,1,108,84Zm128,44A108,108,0,1,1,128,20,108.12,108.12,0,0,1,236,128Zm-24,0a84,84,0,1,0-84,84A84.09,84.09,0,0,0,212,128Zm-72,36.68V132a20,20,0,0,0-20-20,12,12,0,0,0-4,23.32V168a20,20,0,0,0,20,20,12,12,0,0,0,4-23.32Z\"/></symbol>\n <symbol id=\"juno-i-link\" viewBox=\"0 0 256 256\"><path d=\"M117.18,188.74a12,12,0,0,1,0,17l-5.12,5.12A58.26,58.26,0,0,1,70.6,228h0A58.62,58.62,0,0,1,29.14,127.92L63.89,93.17a58.64,58.64,0,0,1,98.56,28.11,12,12,0,1,1-23.37,5.44,34.65,34.65,0,0,0-58.22-16.58L46.11,144.89A34.62,34.62,0,0,0,70.57,204h0a34.41,34.41,0,0,0,24.49-10.14l5.11-5.12A12,12,0,0,1,117.18,188.74ZM226.83,45.17a58.65,58.65,0,0,0-82.93,0l-5.11,5.11a12,12,0,0,0,17,17l5.12-5.12a34.63,34.63,0,1,1,49,49L175.1,145.86A34.39,34.39,0,0,1,150.61,156h0a34.63,34.63,0,0,1-33.69-26.72,12,12,0,0,0-23.38,5.44A58.64,58.64,0,0,0,150.56,180h.05a58.28,58.28,0,0,0,41.47-17.17l34.75-34.75a58.62,58.62,0,0,0,0-82.91Z\"/></symbol>\n <symbol id=\"juno-i-list\" viewBox=\"0 0 256 256\"><path d=\"M228,128a12,12,0,0,1-12,12H40a12,12,0,0,1,0-24H216A12,12,0,0,1,228,128ZM40,76H216a12,12,0,0,0,0-24H40a12,12,0,0,0,0,24ZM216,180H40a12,12,0,0,0,0,24H216a12,12,0,0,0,0-24Z\"/></symbol>\n <symbol id=\"juno-i-lock-open\" viewBox=\"0 0 256 256\"><path d=\"M208,76H100V56a28,28,0,0,1,28-28c13.51,0,25.65,9.62,28.24,22.39a12,12,0,1,0,23.52-4.78C174.87,21.5,153.1,4,128,4A52.06,52.06,0,0,0,76,56V76H48A20,20,0,0,0,28,96V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V96A20,20,0,0,0,208,76Zm-4,128H52V100H204Zm-92-52a16,16,0,1,1,16,16A16,16,0,0,1,112,152Z\"/></symbol>\n <symbol id=\"juno-i-lock\" viewBox=\"0 0 256 256\"><path d=\"M208,76H180V56A52,52,0,0,0,76,56V76H48A20,20,0,0,0,28,96V208a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V96A20,20,0,0,0,208,76ZM100,56a28,28,0,0,1,56,0V76H100ZM204,204H52V100H204Zm-60-52a16,16,0,1,1-16-16A16,16,0,0,1,144,152Z\"/></symbol>\n <symbol id=\"juno-i-magnifying-glass\" viewBox=\"0 0 256 256\"><path d=\"M232.49,215.51,185,168a92.12,92.12,0,1,0-17,17l47.53,47.54a12,12,0,0,0,17-17ZM44,112a68,68,0,1,1,68,68A68.07,68.07,0,0,1,44,112Z\"/></symbol>\n <symbol id=\"juno-i-minus\" viewBox=\"0 0 256 256\"><path d=\"M228,128a12,12,0,0,1-12,12H40a12,12,0,0,1,0-24H216A12,12,0,0,1,228,128Z\"/></symbol>\n <symbol id=\"juno-i-moon\" viewBox=\"0 0 256 256\"><path d=\"M236.37,139.4a12,12,0,0,0-12-3A84.07,84.07,0,0,1,119.6,31.59a12,12,0,0,0-15-15A108.86,108.86,0,0,0,49.69,55.07,108,108,0,0,0,136,228a107.09,107.09,0,0,0,64.93-21.69,108.86,108.86,0,0,0,38.44-54.94A12,12,0,0,0,236.37,139.4Zm-49.88,47.74A84,84,0,0,1,68.86,69.51,84.93,84.93,0,0,1,92.27,48.29Q92,52.13,92,56A108.12,108.12,0,0,0,200,164q3.87,0,7.71-.27A84.79,84.79,0,0,1,186.49,187.14Z\"/></symbol>\n <symbol id=\"juno-i-pencil-simple\" viewBox=\"0 0 256 256\"><path d=\"M230.14,70.54,185.46,25.85a20,20,0,0,0-28.29,0L33.86,149.17A19.85,19.85,0,0,0,28,163.31V208a20,20,0,0,0,20,20H92.69a19.86,19.86,0,0,0,14.14-5.86L230.14,98.82a20,20,0,0,0,0-28.28ZM91,204H52V165l84-84,39,39ZM192,103,153,64l18.34-18.34,39,39Z\"/></symbol>\n <symbol id=\"juno-i-play\" viewBox=\"0 0 256 256\"><path d=\"M234.49,111.07,90.41,22.94A20,20,0,0,0,60,39.87V216.13a20,20,0,0,0,30.41,16.93l144.08-88.13a19.82,19.82,0,0,0,0-33.86ZM84,208.85V47.15L216.16,128Z\"/></symbol>\n <symbol id=\"juno-i-plus\" viewBox=\"0 0 256 256\"><path d=\"M228,128a12,12,0,0,1-12,12H140v76a12,12,0,0,1-24,0V140H40a12,12,0,0,1,0-24h76V40a12,12,0,0,1,24,0v76h76A12,12,0,0,1,228,128Z\"/></symbol>\n <symbol id=\"juno-i-puzzle-piece\" viewBox=\"0 0 256 256\"><path d=\"M222.41,155.16a12,12,0,0,0-11.56-.69A16,16,0,0,1,188,139,16.2,16.2,0,0,1,202.8,124a15.83,15.83,0,0,1,8,1.5A12,12,0,0,0,228,114.7V72a20,20,0,0,0-20-20H176a40.15,40.15,0,0,0-12.62-29.16,39.67,39.67,0,0,0-29.94-10.76,40.08,40.08,0,0,0-37.34,37C96,50.07,96,51,96,52H64A20,20,0,0,0,44,72v28a40.15,40.15,0,0,0-29.16,12.62A40,40,0,0,0,41.1,179.9a28.3,28.3,0,0,0,2.9.1v28a20,20,0,0,0,20,20H208a20,20,0,0,0,20-20V165.31A12,12,0,0,0,222.41,155.16ZM204,204H68V165.31a12,12,0,0,0-17.15-10.84A15.9,15.9,0,0,1,42.8,156,16.2,16.2,0,0,1,28,141.06a16,16,0,0,1,22.82-15.52A12,12,0,0,0,68,114.7V76h42.7a12,12,0,0,0,10.83-17.15A15.9,15.9,0,0,1,120,50.8,16.19,16.19,0,0,1,134.94,36a16,16,0,0,1,15.53,22.81A12,12,0,0,0,161.31,76H204v24c-1,0-1.93,0-2.9.11A40,40,0,0,0,204,180h0Z\"/></symbol>\n <symbol id=\"juno-i-question\" viewBox=\"0 0 256 256\"><path d=\"M144,180a16,16,0,1,1-16-16A16,16,0,0,1,144,180Zm92-52A108,108,0,1,1,128,20,108.12,108.12,0,0,1,236,128Zm-24,0a84,84,0,1,0-84,84A84.09,84.09,0,0,0,212,128ZM128,64c-24.26,0-44,17.94-44,40v4a12,12,0,0,0,24,0v-4c0-8.82,9-16,20-16s20,7.18,20,16-9,16-20,16a12,12,0,0,0-12,12v8a12,12,0,0,0,23.73,2.56C158.31,137.88,172,122.37,172,104,172,81.94,152.26,64,128,64Z\"/></symbol>\n <symbol id=\"juno-i-sliders-horizontal\" viewBox=\"0 0 256 256\"><path d=\"M40,92H70.06a36,36,0,0,0,67.88,0H216a12,12,0,0,0,0-24H137.94a36,36,0,0,0-67.88,0H40a12,12,0,0,0,0,24Zm64-24A12,12,0,1,1,92,80,12,12,0,0,1,104,68Zm112,96H201.94a36,36,0,0,0-67.88,0H40a12,12,0,0,0,0,24h94.06a36,36,0,0,0,67.88,0H216a12,12,0,0,0,0-24Zm-48,24a12,12,0,1,1,12-12A12,12,0,0,1,168,188Z\"/></symbol>\n <symbol id=\"juno-i-sliders\" viewBox=\"0 0 256 256\"><path d=\"M68,102.06V40a12,12,0,0,0-24,0v62.06a36,36,0,0,0,0,67.88V216a12,12,0,0,0,24,0V169.94a36,36,0,0,0,0-67.88ZM56,148a12,12,0,1,1,12-12A12,12,0,0,1,56,148ZM164,88a36.07,36.07,0,0,0-24-33.94V40a12,12,0,0,0-24,0V54.06a36,36,0,0,0,0,67.88V216a12,12,0,0,0,24,0V121.94A36.07,36.07,0,0,0,164,88Zm-36,12a12,12,0,1,1,12-12A12,12,0,0,1,128,100Zm108,68a36.07,36.07,0,0,0-24-33.94V40a12,12,0,0,0-24,0v94.06a36,36,0,0,0,0,67.88V216a12,12,0,0,0,24,0V201.94A36.07,36.07,0,0,0,236,168Zm-36,12a12,12,0,1,1,12-12A12,12,0,0,1,200,180Z\"/></symbol>\n <symbol id=\"juno-i-squares-four\" viewBox=\"0 0 256 256\"><path d=\"M100,36H56A20,20,0,0,0,36,56v44a20,20,0,0,0,20,20h44a20,20,0,0,0,20-20V56A20,20,0,0,0,100,36ZM96,96H60V60H96ZM200,36H156a20,20,0,0,0-20,20v44a20,20,0,0,0,20,20h44a20,20,0,0,0,20-20V56A20,20,0,0,0,200,36Zm-4,60H160V60h36Zm-96,40H56a20,20,0,0,0-20,20v44a20,20,0,0,0,20,20h44a20,20,0,0,0,20-20V156A20,20,0,0,0,100,136Zm-4,60H60V160H96Zm104-60H156a20,20,0,0,0-20,20v44a20,20,0,0,0,20,20h44a20,20,0,0,0,20-20V156A20,20,0,0,0,200,136Zm-4,60H160V160h36Z\"/></symbol>\n <symbol id=\"juno-i-star\" viewBox=\"0 0 256 256\"><path d=\"M243,96a20.33,20.33,0,0,0-17.74-14l-56.59-4.57L146.83,24.62a20.36,20.36,0,0,0-37.66,0L87.35,77.44,30.76,82A20.45,20.45,0,0,0,19.1,117.88l43.18,37.24-13.2,55.7A20.37,20.37,0,0,0,79.57,233L128,203.19,176.43,233a20.39,20.39,0,0,0,30.49-22.15l-13.2-55.7,43.18-37.24A20.43,20.43,0,0,0,243,96ZM172.53,141.7a12,12,0,0,0-3.84,11.86L181.58,208l-47.29-29.08a12,12,0,0,0-12.58,0L74.42,208l12.89-54.4a12,12,0,0,0-3.84-11.86L41.2,105.24l55.4-4.47a12,12,0,0,0,10.13-7.38L128,41.89l21.27,51.5a12,12,0,0,0,10.13,7.38l55.4,4.47Z\"/></symbol>\n <symbol id=\"juno-i-sun\" viewBox=\"0 0 256 256\"><path d=\"M116,36V20a12,12,0,0,1,24,0V36a12,12,0,0,1-24,0Zm80,92a68,68,0,1,1-68-68A68.07,68.07,0,0,1,196,128Zm-24,0a44,44,0,1,0-44,44A44.05,44.05,0,0,0,172,128ZM51.51,68.49a12,12,0,1,0,17-17l-12-12a12,12,0,0,0-17,17Zm0,119-12,12a12,12,0,0,0,17,17l12-12a12,12,0,1,0-17-17ZM196,72a12,12,0,0,0,8.49-3.51l12-12a12,12,0,0,0-17-17l-12,12A12,12,0,0,0,196,72Zm8.49,115.51a12,12,0,0,0-17,17l12,12a12,12,0,0,0,17-17ZM48,128a12,12,0,0,0-12-12H20a12,12,0,0,0,0,24H36A12,12,0,0,0,48,128Zm80,80a12,12,0,0,0-12,12v16a12,12,0,0,0,24,0V220A12,12,0,0,0,128,208Zm108-92H220a12,12,0,0,0,0,24h16a12,12,0,0,0,0-24Z\"/></symbol>\n <symbol id=\"juno-i-trash\" viewBox=\"0 0 256 256\"><path d=\"M216,48H180V36A28,28,0,0,0,152,8H104A28,28,0,0,0,76,36V48H40a12,12,0,0,0,0,24h4V208a20,20,0,0,0,20,20H192a20,20,0,0,0,20-20V72h4a12,12,0,0,0,0-24ZM100,36a4,4,0,0,1,4-4h48a4,4,0,0,1,4,4V48H100Zm88,168H68V72H188ZM116,104v64a12,12,0,0,1-24,0V104a12,12,0,0,1,24,0Zm48,0v64a12,12,0,0,1-24,0V104a12,12,0,0,1,24,0Z\"/></symbol>\n <symbol id=\"juno-i-upload-simple\" viewBox=\"0 0 256 256\"><path d=\"M228,144v64a12,12,0,0,1-12,12H40a12,12,0,0,1-12-12V144a12,12,0,0,1,24,0v52H204V144a12,12,0,0,1,24,0ZM96.49,80.49,116,61v83a12,12,0,0,0,24,0V61l19.51,19.52a12,12,0,1,0,17-17l-40-40a12,12,0,0,0-17,0l-40,40a12,12,0,1,0,17,17Z\"/></symbol>\n <symbol id=\"juno-i-upload\" viewBox=\"0 0 256 256\"><path d=\"M188,184a16,16,0,1,1,16-16A16,16,0,0,1,188,184Zm36-68H180a12,12,0,0,0,0,24h40v56H36V140H76a12,12,0,0,0,0-24H32a20,20,0,0,0-20,20v64a20,20,0,0,0,20,20H224a20,20,0,0,0,20-20V136A20,20,0,0,0,224,116ZM88.49,80.49,116,53v75a12,12,0,0,0,24,0V53l27.51,27.52a12,12,0,1,0,17-17l-48-48a12,12,0,0,0-17,0l-48,48a12,12,0,1,0,17,17Z\"/></symbol>\n <symbol id=\"juno-i-user\" viewBox=\"0 0 256 256\"><path d=\"M234.38,210a123.36,123.36,0,0,0-60.78-53.23,76,76,0,1,0-91.2,0A123.36,123.36,0,0,0,21.62,210a12,12,0,1,0,20.77,12c18.12-31.32,50.12-50,85.61-50s67.49,18.69,85.61,50a12,12,0,0,0,20.77-12ZM76,96a52,52,0,1,1,52,52A52.06,52.06,0,0,1,76,96Z\"/></symbol>\n <symbol id=\"juno-i-users\" viewBox=\"0 0 256 256\"><path d=\"M125.18,156.94a64,64,0,1,0-82.36,0,100.23,100.23,0,0,0-39.49,32,12,12,0,0,0,19.35,14.2,76,76,0,0,1,122.64,0,12,12,0,0,0,19.36-14.2A100.33,100.33,0,0,0,125.18,156.94ZM44,108a40,40,0,1,1,40,40A40,40,0,0,1,44,108Zm206.1,97.67a12,12,0,0,1-16.78-2.57A76.31,76.31,0,0,0,172,172a12,12,0,0,1,0-24,40,40,0,1,0-10.3-78.67,12,12,0,1,1-6.16-23.19,64,64,0,0,1,57.64,110.8,100.23,100.23,0,0,1,39.49,32A12,12,0,0,1,250.1,205.67Z\"/></symbol>\n <symbol id=\"juno-i-warning-circle\" viewBox=\"0 0 256 256\"><path d=\"M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm0,192a84,84,0,1,1,84-84A84.09,84.09,0,0,1,128,212Zm-12-80V80a12,12,0,0,1,24,0v52a12,12,0,0,1-24,0Zm28,40a16,16,0,1,1-16-16A16,16,0,0,1,144,172Z\"/></symbol>\n <symbol id=\"juno-i-warning\" viewBox=\"0 0 256 256\"><path d=\"M240.26,186.1,152.81,34.23h0a28.74,28.74,0,0,0-49.62,0L15.74,186.1a27.45,27.45,0,0,0,0,27.71A28.31,28.31,0,0,0,40.55,228h174.9a28.31,28.31,0,0,0,24.79-14.19A27.45,27.45,0,0,0,240.26,186.1Zm-20.8,15.7a4.46,4.46,0,0,1-4,2.2H40.55a4.46,4.46,0,0,1-4-2.2,3.56,3.56,0,0,1,0-3.73L124,46.2a4.77,4.77,0,0,1,8,0l87.44,151.87A3.56,3.56,0,0,1,219.46,201.8ZM116,136V104a12,12,0,0,1,24,0v32a12,12,0,0,1-24,0Zm28,40a16,16,0,1,1-16-16A16,16,0,0,1,144,176Z\"/></symbol>\n <symbol id=\"juno-i-x-circle\" viewBox=\"0 0 256 256\"><path d=\"M168.49,104.49,145,128l23.52,23.51a12,12,0,0,1-17,17L128,145l-23.51,23.52a12,12,0,0,1-17-17L111,128,87.51,104.49a12,12,0,0,1,17-17L128,111l23.51-23.52a12,12,0,0,1,17,17ZM236,128A108,108,0,1,1,128,20,108.12,108.12,0,0,1,236,128Zm-24,0a84,84,0,1,0-84,84A84.09,84.09,0,0,0,212,128Z\"/></symbol>\n <symbol id=\"juno-i-x\" viewBox=\"0 0 256 256\"><path d=\"M208.49,191.51a12,12,0,0,1-17,17L128,145,64.49,208.49a12,12,0,0,1-17-17L111,128,47.51,64.49a12,12,0,0,1,17-17L128,111l63.51-63.52a12,12,0,0,1,17,17L145,128Z\"/></symbol>\n</svg>\n";
7
11
 
8
- /** Inject the sprite into `doc` once (id-guarded, safe to call repeatedly). */
12
+ /** Inject the full sprite into `doc` once (id-guarded, safe to call repeatedly). */
9
13
  export function installJunoIcons(doc = typeof document !== 'undefined' ? document : undefined) {
10
- if (!doc || doc.getElementById('juno-icon-sprite')) return;
11
- const holder = doc.createElement('div');
12
- holder.id = 'juno-icon-sprite';
13
- holder.style.display = 'none';
14
- holder.setAttribute('aria-hidden', 'true');
15
- holder.innerHTML = SPRITE;
16
- (doc.body || doc.documentElement).prepend(holder);
14
+ installSprite(SPRITE, doc);
17
15
  }
18
16
 
19
17
  // Auto-install on import in a browser (no-op server-side / pre-hydration).
@@ -0,0 +1,26 @@
1
+ // junoui — icon sprite injector, no payload. Generated; do not edit.
2
+ // Same-document injection, because Safari drops external <use href="file.svg#id">
3
+ // refs intermittently. Pair with junoui/subset when you ship a subset:
4
+ // import { installSprite } from '@junoput01/junoui/icons/install';
5
+ // import sprite from 'virtual:my-icon-subset';
6
+ // installSprite(sprite);
7
+ export const JUNO_SPRITE_ID = 'juno-icon-sprite';
8
+
9
+ /**
10
+ * Inject `svg` (sprite source) into `doc` once. Id-guarded, so calling it
11
+ * repeatedly — or alongside junoui/icons/inline — installs exactly one sprite:
12
+ * whichever ran first wins, and a second call is a no-op rather than a second
13
+ * hidden holder shadowing the first.
14
+ */
15
+ export function installSprite(svg, doc = typeof document !== 'undefined' ? document : undefined) {
16
+ if (!doc || !svg || doc.getElementById(JUNO_SPRITE_ID)) return false;
17
+ const holder = doc.createElement('div');
18
+ holder.id = JUNO_SPRITE_ID;
19
+ holder.style.display = 'none';
20
+ holder.setAttribute('aria-hidden', 'true');
21
+ holder.innerHTML = svg;
22
+ (doc.body || doc.documentElement).prepend(holder);
23
+ return true;
24
+ }
25
+
26
+ export default installSprite;
@@ -0,0 +1,173 @@
1
+ # Browser support
2
+
3
+ What junoui requires, what it loses gracefully below that, and the short list of
4
+ things that genuinely **break**. Stated because the failures are otherwise
5
+ silent: an engine that does not understand a CSS at-rule, property or selector
6
+ **drops it without reporting anything**. Nobody finds out from a console; they
7
+ find out from a screenshot.
8
+
9
+ Audited against `dist/css/juno.css` at v0.5.0 (2026-08-15).
10
+
11
+ ## The floor
12
+
13
+ | | Safari / iOS | Chrome / Edge | Firefox |
14
+ | ------------------------------------------------------------ | ------------ | ------------- | ---------------------------------- |
15
+ | **Hard floor** — below this, things break | **17.0** | **114** | **125** |
16
+ | **Supported floor** — everything works, minor polish missing | **17.5** | 117 | 129 |
17
+ | **Full fidelity** — nothing missing | **26.0** | 125 | — (anchor positioning not shipped) |
18
+
19
+ **junoui's supported floor is Safari / iOS 17.5.** That is the version to put in
20
+ a consumer's own support matrix.
21
+
22
+ Read the three rows as three different questions:
23
+
24
+ - Below **17.0** the Popover API is absent and junoui's overlay surfaces stop
25
+ being overlays. See [What breaks](#what-breaks) — this is not a polish
26
+ question, and one of the two failure modes is worse than "the menu does not
27
+ open".
28
+ - Between **17.0 and 17.5** everything functions; entry animations are missing.
29
+ - Between **17.5 and 26.0** everything functions and animates; anchored
30
+ surfaces (menu, popover, tooltip) land at their static position instead of
31
+ next to their trigger, because CSS anchor positioning is a Safari 26 feature.
32
+ This is a real visual defect, not a nicety — but the surface still opens,
33
+ still closes, still light-dismisses, and the app can pin it (see below).
34
+
35
+ `package.json` carries a `browserslist` set to the **hard floor**, so tooling
36
+ reads the line below which junoui is broken rather than the line below which it
37
+ is imperfect. junoui itself runs no autoprefixer or transpiler — the field is a
38
+ declaration for consumers' build tools, not something junoui acts on.
39
+
40
+ ## Feature audit
41
+
42
+ Counts are occurrences in the built bundle.
43
+
44
+ | Feature | Uses | Safari / iOS | Below that |
45
+ | ------------------------------------------------------------------------------------- | ---- | ------------ | -------------------------------------------------------------------------------------------- |
46
+ | CSS anchor positioning (`position-area`, `position-try-fallbacks`, `position-anchor`) | 10 | **26.0** | degrades — [misplaced surfaces](#anchored-surfaces-lose-their-anchor) |
47
+ | `scrollbar-width` | 4 | 18.2 | **guarded** — `::-webkit-scrollbar` fallback ships |
48
+ | `backdrop-filter` (unprefixed) | 14 | 18.0 | **guarded** — `-webkit-backdrop-filter` ships beside every use |
49
+ | `@starting-style` | 10 | 17.5 | degrades — no entry animation |
50
+ | `transition-behavior` / `overlay` + `allow-discrete` | 10 | 17.4 | degrades — surfaces pop instead of fading |
51
+ | **Popover API** (`popover`, `popovertarget`, `:popover-open`) | 31 | **17.0** | **breaks** |
52
+ | `prefers-reduced-transparency` | 3 | 17.0 | degrades — blur stays on for users who asked it off |
53
+ | `@property` | 1 | 16.4 | degrades — the gauge jumps to its value instead of sweeping |
54
+ | `color-mix()` | 28 | 16.2 | degrades — role tints vanish (see the caveat below) |
55
+ | `@container` | 2 | 16.0 | degrades — card and table stop reflowing at narrow widths |
56
+ | `:has()` | 5 | 15.4 | degrades — the collapsible pillbar never collapses |
57
+ | `dvh` / `lvh` | 10 | 15.4 | breaks layout — `block-size` falls back to `auto`, the app shell collapses to content height |
58
+ | `oklch()` | 190 | 15.4 | breaks colour — every token value is `oklch()` |
59
+ | `aspect-ratio` | 7 | 15.0 | degrades — media tiles lose their ratio |
60
+ | `@supports selector()` | 1 | 14.1 | the Popover guard below is inert (fails open to today's behaviour) |
61
+
62
+ Two rows deserve a note.
63
+
64
+ **`color-mix()` is listed as a degrade, and that is technically true and
65
+ practically uncomfortable.** All 28 uses are tints, borders and shadows built
66
+ from `var(--juno-role)`; when the function is unsupported the declaration is
67
+ invalid and the property falls back to `transparent` or the previous cascade
68
+ value. Text and icon colour, which take `var(--juno-role)` directly, survive. So
69
+ nothing breaks — but role tinting is exactly the thing junoui exists to encode,
70
+ and losing it erodes the contract even while the layout holds. It sits below the
71
+ floor; it is listed so nobody discovers it by shipping to iOS 16.
72
+
73
+ **`oklch()` and `dvh` are below every floor in this document** and are listed for
74
+ completeness. If you are on iOS 15 you are not using junoui.
75
+
76
+ ## What breaks
77
+
78
+ Exactly one thing, at exactly one version, and it has two failure modes.
79
+
80
+ ### Popover API, Safari / iOS < 17.0
81
+
82
+ junoui's `.juno-menu`, `.juno-popover` and the top-layer `.juno-tooltip__bubble`
83
+ are built on the native Popover API. The open/close, light-dismiss and ESC
84
+ behaviour is the platform's; junoui ships only the surface. On an engine without
85
+ it:
86
+
87
+ 1. **They never open.** `popovertarget` does nothing, so there is no way to
88
+ reveal the panel. The pillbar's overflow slot routes to `.juno-menu`, so the
89
+ overflow items are unreachable — which on a phone is where the secondary
90
+ navigation lives.
91
+ 2. **Worse: `.juno-menu` and `.juno-popover` become invisible click-eaters.**
92
+ The UA rule that hides a closed popover (`display: none` on any `[popover]`
93
+ that is not `:popover-open`) does not exist on an unsupporting engine, so the
94
+ panel participates in layout. junoui's own base rule sets `opacity: 0`, so it is
95
+ invisible, but both surfaces are `position: fixed` with no `pointer-events`
96
+ reset. The result is a 256–280 px invisible fixed panel sitting over the page
97
+ at its static position, swallowing taps on whatever it covers.
98
+
99
+ (`.juno-tooltip__bubble` escapes this: it inherits `pointer-events: none`
100
+ from the CSS-only tooltip rule it shares.)
101
+
102
+ Failure mode 2 is the one that justifies a guard. A feature that is missing is
103
+ an inconvenience; a feature that is missing and eats input is a bug report that
104
+ looks like something else entirely. `base.css` therefore ships:
105
+
106
+ ```css
107
+ @supports not selector(:popover-open) {
108
+ .juno-menu[popover],
109
+ .juno-popover[popover],
110
+ .juno-tooltip__bubble[popover] {
111
+ display: none;
112
+ }
113
+ }
114
+ ```
115
+
116
+ This makes failure mode 1 the _only_ failure mode: the panel is absent instead
117
+ of invisibly present, the page underneath is fully usable, and the app can
118
+ detect the same condition in one line
119
+ (`CSS.supports('selector(:popover-open)')`) and render a non-popover fallback.
120
+
121
+ `@supports selector()` itself needs Safari 14.1; below that the guard's
122
+ condition is invalid, the block is skipped, and behaviour is exactly what it is
123
+ today. Fail-open, and far below any floor here.
124
+
125
+ ### Anchored surfaces lose their anchor
126
+
127
+ Not a break — recorded here because it is the largest visual regression between
128
+ 17.5 and 26.0 and it is easy to mistake for one.
129
+
130
+ `.juno-menu`, `.juno-popover` and the top-layer tooltip position themselves with
131
+ `position-area` / `position-try-fallbacks`, which is Safari **26.0**. Below it
132
+ those declarations drop and the surfaces keep `position: fixed; inset: auto;
133
+ margin: 0` — so they render at their static position rather than beside their
134
+ trigger. They still open, close, light-dismiss and trap nothing.
135
+
136
+ The documented remedy is the app's: pin `inset` on the element's `toggle` event.
137
+ It is already noted in [popover.md](./components/popover.md) and is the reason
138
+ the legacy `.juno-popover-anchor` wrapper is still exported.
139
+
140
+ ## Why this document exists
141
+
142
+ Two silent failures of this exact class shipped and were found by looking at a
143
+ phone, not by any tool:
144
+
145
+ - **`backdrop-filter` unprefixed only.** Unsupported below Safari 18.0, so the
146
+ declaration was dropped and every glass surface — pillbar, dock, modal scrim —
147
+ rendered flat on iOS 17. Fixed 2026-08-05 by shipping
148
+ `-webkit-backdrop-filter` beside each use; both are in the bundle now, and the
149
+ audit table above tracks them as guarded.
150
+ - **`scrollbar-width: none`.** Unsupported below Safari 18.2, so the scrollable
151
+ tab strip kept a visible scrollbar on iOS 17 and 18.0–18.1. Fixed the same day
152
+ with a `::-webkit-scrollbar { display: none }` companion.
153
+
154
+ Neither produced an error. Both were "modern CSS, ships everywhere" right up
155
+ until someone opened the page on the device. That is the argument for stating a
156
+ floor and for auditing against the built bundle rather than against intent: the
157
+ cost of a missing feature is paid by the consumer, silently, at a time and place
158
+ junoui never sees.
159
+
160
+ **When you add a feature below the floor, say so here.** A new at-rule,
161
+ selector or property whose support is newer than Safari 17.5 belongs in the
162
+ audit table with its degrade-or-break verdict, and if the verdict is _break_ it
163
+ needs an `@supports` guard in the same change. Guards are for functional
164
+ failures only — a missing animation does not get one, and wrapping cosmetic
165
+ declarations in `@supports` costs bytes and buys nothing.
166
+
167
+ ## Related
168
+
169
+ - [ios-conformance.md](./ios-conformance.md) — iOS metrics and behaviour: safe
170
+ areas, tap targets, the viewport-unit decision, and the Home-Screen standalone
171
+ letterbox.
172
+ - [getting-started.md](./getting-started.md) — the required `viewport` meta,
173
+ which is a separate silent failure with the same shape.
@@ -48,6 +48,28 @@ Two things depend on it, and both fail **silently**:
48
48
 
49
49
  Details and sources: [ios-conformance.md](./ios-conformance.md).
50
50
 
51
+ ## Browser support
52
+
53
+ | | Safari / iOS | Chrome / Edge | Firefox |
54
+ | ------------------------------------------- | ------------ | ------------- | ------- |
55
+ | **Supported floor** — everything works | **17.5** | 117 | 129 |
56
+ | **Hard floor** — below this, overlays break | 17.0 | 114 | 125 |
57
+
58
+ The hard floor is the Popover API. Below it, `.juno-menu`, `.juno-popover`, the
59
+ top-layer tooltip and the pillbar's overflow slot cannot open; junoui hides them
60
+ with an `@supports` guard so they are absent rather than invisible-and-blocking,
61
+ and your app can branch on `CSS.supports('selector(:popover-open)')`. Between
62
+ the two floors the only loss is entry animations (`@starting-style`). Anchored
63
+ placement for those surfaces is a Safari 26 feature — below it they open at
64
+ their static position, and the app can pin `inset` on the `toggle` event.
65
+
66
+ The hard floor is what `package.json`'s `browserslist` declares. junoui runs no
67
+ autoprefixer of its own; the field is there for your build tooling.
68
+
69
+ **Every one of these failures is silent** — an engine drops CSS it does not
70
+ understand without reporting anything. The full audit, per feature, with the
71
+ degrade-vs-break verdict: [browser-support.md](./browser-support.md).
72
+
51
73
  ## The model
52
74
 
53
75
  - **Palette** — `standard` · `colorblind` · `soft`
@@ -57,3 +57,32 @@ const iconSubset = {
57
57
  Keep the manifest where the app's own type for icon names is derived from it,
58
58
  so a name that is not in the manifest is a **compile** error rather than a
59
59
  blank space at runtime.
60
+
61
+ ## Injecting a subset
62
+
63
+ Safari intermittently drops external sprite refs (`<use href="file.svg#id">`),
64
+ so the reliable path is a **same-document** ref — which means the sprite has to
65
+ live in the document. `junoui/icons/inline` does that for the full set and
66
+ auto-installs on import; a consumer that subsets wants the injection without
67
+ the 25 kB of symbols, so the mechanism ships separately:
68
+
69
+ ```js
70
+ import { installSprite } from '@junoput01/junoui/icons/install';
71
+ import sprite from 'virtual:my-icon-subset'; // your build's subset (see above)
72
+
73
+ installSprite(sprite);
74
+ ```
75
+
76
+ | Export | Carries | Use |
77
+ | ---------------------- | ------------------------- | --------------------------------------------- |
78
+ | `junoui/icons/install` | ~1 kB, no icons | You subset. Pass your own sprite. |
79
+ | `junoui/icons/inline` | the full 66-symbol sprite | You don't subset. Import for the side effect. |
80
+
81
+ - **Both are id-guarded on the same `juno-icon-sprite` holder**, so importing
82
+ the full module alongside a subset does not produce two hidden holders
83
+ shadowing each other — whichever installs first wins and the second call is a
84
+ no-op returning `false`.
85
+ - `installSprite` returns `true` when it injected, `false` when a sprite was
86
+ already present or there is no document (server-side, pre-hydration).
87
+ - Subsetting only pays off if the full module never enters the bundle: import
88
+ `icons/install`, not `icons/inline`.
@@ -7,6 +7,10 @@ Most numbers the design community attributes to Apple are not in Apple's text.
7
7
  This page exists so nobody re-derives folklore, and so nobody "fixes" a correct
8
8
  value into a wrong one later.
9
9
 
10
+ This page is about **metrics and behaviour** on iOS. For _which iOS versions
11
+ junoui runs on at all_ — the supported floor, what degrades below it, and what
12
+ breaks — see [browser-support.md](./browser-support.md).
13
+
10
14
  > **Verifying anything here.** Apple's HIG is a JavaScript app: a plain `curl`
11
15
  > returns an empty shell. Check the backing DocC JSON instead —
12
16
  > `developer.apple.com/tutorials/data/design/human-interface-guidelines/<page>.json`.
@@ -157,11 +161,10 @@ to `lv*`** — that is the spec-level cause of the classic `100vh` overflow: a
157
161
  junoui uses **zero raw `vh`**. Full-height surfaces use `dvh`
158
162
  (`layout.css`, `drawer.css`) and `85dvh` caps the bottom sheet.
159
163
 
160
- Caveats worth knowing before changing any of that:
164
+ Caveats that constrain the choice:
161
165
 
162
166
  - `dv*` is explicitly **not stable** and not guaranteed to update every frame,
163
- so it can churn while the address bar collapses. `sv*` is the calm choice when
164
- a surface must never overflow.
167
+ so it can churn while the address bar collapses. `sv*` never overflows.
165
168
  - iOS shipped viewport-unit bugs into the **iOS 26** era: Safari 26.0 fixed
166
169
  `lvh`/`vh` being sized against the _small_ viewport in `SFSafariViewController`.
167
170
  The underlying WebKit bug (255708, filed 2023) is **still open**, so Apple's
@@ -176,6 +179,193 @@ Caveats worth knowing before changing any of that:
176
179
  [Safari 26.0 release notes](https://developer.apple.com/documentation/safari-release-notes/safari-26-release-notes),
177
180
  [WebKit bug 255708](https://bugs.webkit.org/show_bug.cgi?id=255708).
178
181
 
182
+ ### The decision: `dvh` stays at both call sites
183
+
184
+ Decided 2026-08-15 (ticket 20260803-033, step 1). `dvh` is kept for **both**
185
+ `.juno-app-shell` and `.juno-drawer`, against the "`sv*` is the calm choice"
186
+ instinct above. The reasoning below is the part to read: it generalises, the
187
+ verdict does not.
188
+
189
+ **First, price the two options at the only moment they differ** — when browser
190
+ chrome retracts. There is no other moment. Before it and after it, whichever
191
+ unit you picked is simply the current viewport.
192
+
193
+ | | what it costs when chrome retracts |
194
+ | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
195
+ | `dvh` | The surface **grows**, so it relayouts. `dv*` is not frame-guaranteed, so that growth can arrive late or in steps. If the surface contains a scroller, the scroll port changes height _during the scroll that caused the retraction_. |
196
+ | `svh` | The surface **does not move** — and is now short by `lvh − svh`. That gap is permanent for the rest of the session, and it is at the bottom edge, where the page background shows through under whatever the surface pinned there. |
197
+
198
+ So this is not "stable vs twitchy". It is **one relayout** against **a permanent
199
+ dead strip**. Pick by asking which of those the surface can afford.
200
+
201
+ **`.juno-app-shell` — `block-size: 100dvh` (`layout.css`).** The shell's whole
202
+ job is to hold the dock against the bottom edge. Under `svh` the dock detaches
203
+ from that edge the first time the user scrolls and stays detached, floating
204
+ `lvh − svh` px up with page background beneath it — a visible defect on every
205
+ subsequent frame, under the primary navigation. The `dvh` relayout it avoids is
206
+ cheap here by construction: the shell is `overflow: hidden` and the scrolling
207
+ lives in `.juno-app-shell__main`, so growing the shell grows the scroll port and
208
+ moves the dock. **No text reflows** — no line breaking, no measured content, no
209
+ intrinsic sizing is touched. One cheap relayout beats a permanent gap.
210
+
211
+ **`.juno-drawer` — `block-size: 100dvh; max-block-size: 100dvh`
212
+ (`drawer.css`), and the same for the `--bottom` sheet's `60dvh`/`92dvh` and the
213
+ modal's `85dvh` cap.** Here the `dv*` instability **cannot bite at all**, and
214
+ that is the whole argument. The drawer is a `<dialog>` opened with
215
+ `showModal()`: it is in the top layer and the document beneath it is inert. iOS
216
+ Safari retracts and expands chrome in response to _document_ scroll — an inner
217
+ overflow scroller does not drive it — so chrome cannot change state for the
218
+ drawer's whole lifetime. `dvh` therefore never churns, and `svh` cannot prevent
219
+ an overflow that cannot happen. What `svh` _would_ still do is leave the gap:
220
+ open the drawer after scrolling (the ordinary case — you scroll, then reach for
221
+ the menu) and chrome is already retracted, so a `svh` drawer stops
222
+ `lvh − svh` px short of the bottom of a screen it is supposed to fill. `svh`
223
+ here is a pure loss.
224
+
225
+ > **The exception, and it is the app's to own.** A `<dialog>` opened with the
226
+ > `open` **attribute** instead of `showModal()` is _not_ in the top layer and
227
+ > does **not** block page scroll. Behind such a drawer the page scrolls, chrome
228
+ > retracts, and the churn is live. junoui's CSS cannot tell the two open paths
229
+ > apart. The drawer is documented as `showModal()`
230
+ > ([drawer.md](./components/drawer.md)); open it non-modally and the churn is yours.
231
+
232
+ ### The rule, for a component that does not exist yet
233
+
234
+ Ask one question — **can browser chrome change state while this surface is on
235
+ screen?** — and then:
236
+
237
+ 1. **It can, and the surface is anchored to the bottom edge** (or contains
238
+ something that is: a dock, a pillbar, a sticky footer) → **`dvh`**. You are
239
+ buying edge-adherence and paying one relayout per chrome transition.
240
+ 2. **It can, and the surface must not resize once laid out** — content whose
241
+ height JS measures, a canvas, an animation mid-flight, anything where a
242
+ late reflow is worse than a gap → **`svh`**. You are buying stability and
243
+ paying up to `lvh − svh` of dead space, permanently.
244
+ 3. **It cannot** — top-layer surfaces (`showModal()` dialogs, `popover`), where
245
+ the page beneath is inert → **`dvh`**, always. `svh` there buys nothing and
246
+ still pays the gap on any surface opened while chrome is retracted.
247
+ 4. **Never `vh` / `lvh` for a height that must fit.** `vh == lvh` sizes as if
248
+ chrome were retracted, so on a page where it is _not_, the box overflows by
249
+ exactly the chrome's height. That is the classic `100vh` bug and it is a
250
+ spec consequence, not a browser bug. The one legitimate `lvh` in this
251
+ codebase is the standalone unlock's spacer (`base.css`), which is _supposed_
252
+ to overflow.
253
+
254
+ **In `display-mode: standalone` this whole decision is moot** — there is no
255
+ retractable chrome, so `svh == dvh`. Measured on the device: `100dvh`,
256
+ `100svh` and `100%` all resolve to 812 while `100lvh` and `100vh` resolve to
257
+ 874, which is the letterbox defect below, not a chrome transition.
258
+
259
+ ### A note on raw `vw`
260
+
261
+ "Zero raw `vh`" is about **`vh`**. junoui does use raw `vw` in five places
262
+ (`layout.css:18`, `pillbar.css:231`, `popover.css:33`, `toast.css:26`,
263
+ `drawer.css:116`) and that is deliberate: horizontal chrome does not retract,
264
+ so `vw`/`lvw`/`svw`/`dvw` are the same number and the `vh` trap has no
265
+ horizontal twin. The real `vw` hazard is different — `100vw` includes the
266
+ classic scrollbar gutter, so a full-bleed `100vw` box overflows a desktop page
267
+ that has a scrollbar. Every call site above either caps well below `100vw`
268
+ (`min(…, 240px)`, `min(360px, …)`, `85vw`, `clamp()`) or subtracts more than a
269
+ scrollbar's width (`calc(100vw - var(--juno-space-24))`). Keep it that way; a
270
+ bare `inline-size: 100vw` is a bug.
271
+
272
+ ## Home-Screen standalone: the letterbox, and why `base.css` unlocks it
273
+
274
+ **The fact, and it is the most expensive thing this codebase has learned about
275
+ iOS: in `display-mode: standalone`, iOS sizes the window from the document's
276
+ RESTING scrollability at launch, and letterboxes a document that cannot scroll
277
+ by exactly `env(safe-area-inset-top)`.**
278
+
279
+ Measured, not inferred. iPhone 16 Pro (402×874 pt), iOS 18.7 / Safari 26.6:
280
+
281
+ | | |
282
+ | ---------------------------------------------- | ------- |
283
+ | `screen.height` | **874** |
284
+ | `window.innerHeight` · `visualViewport.height` | **812** |
285
+ | `100dvh` · `100svh` · `100%` | **812** |
286
+ | `100lvh` · `100vh` | **874** |
287
+ | `env(safe-area-inset-top)` | **62** |
288
+ | `env(safe-area-inset-bottom)` | 34 |
289
+ | `window.screenY` | 0 |
290
+
291
+ `874 − 812 = 62 = env(safe-area-inset-top)`, exactly. WebKit sizes the
292
+ standalone window as if a retractable toolbar existed, subtracts its height,
293
+ pins the window to the **top**, and then never covers the strip it reserved — so
294
+ the bottom 62 px of the display sits outside the web view and paints black on
295
+ every screen. This is a spec violation on its face: in standalone there is no
296
+ retractable browser UI, so the large and dynamic viewports **must** be equal
297
+ (css-values-4 §6.1.2.1), and here they differ by 62.
298
+
299
+ ### It is the resting structure, and only the resting structure
300
+
301
+ One install, four document structures, switched by a pill and **persisted across
302
+ cold launches** (`localStorage`), verdict taken per launch:
303
+
304
+ | document structure at rest | window |
305
+ | --------------------------------------------------------------------------------------- | --------------------- |
306
+ | document scrolls normally | **874** — full screen |
307
+ | fixed shell, an inner scroller, document cannot scroll | 812 — letterboxed |
308
+ | fixed shell, nothing scrollable anywhere | 812 — letterboxed |
309
+ | fixed shell **+ the document left scrollable behind it** by an invisible in-flow spacer | **874** — full screen |
310
+
311
+ **Transient scrollability is not enough.** Seven in-page interventions across
312
+ five controlled runs — with a placebo pinned to the first slot, rotation of the
313
+ rest, and `prior`/`during`/`afterUndo` sampling — all measured 812. Every one of
314
+ them varied scrollability for ~300 ms mid-session and undid itself. iOS samples
315
+ the structure at launch; the axis that decides the window was never varied.
316
+
317
+ Two device-proven negatives, recorded so nobody spends another round on them:
318
+
319
+ - **`apple-mobile-web-app-status-bar-style` makes no difference.** `black` and
320
+ `black-translucent` were each tested with a fresh Home-Screen install. Both
321
+ letterbox identically.
322
+ - **Nothing applied after first paint reaches it** — see the seven interventions
323
+ above. The window does correct itself to 874 spontaneously, between 6 seconds
324
+ and 43 minutes after navigation, and then holds for the life of that document;
325
+ a reload starts a new document, which starts letterboxed again.
326
+
327
+ ### What junoui does about it
328
+
329
+ `base.css` carries the **iOS standalone letterbox unlock**: keep the document
330
+ scrollable behind the app, using an invisible `body::after` spacer taller than
331
+ the large viewport, so the document always overflows whatever window iOS grants.
332
+ `overscroll-behavior: none` stops the ghost scroller rubber-banding; apps put
333
+ `overscroll-behavior: contain` on their real scrollers so an inner fling never
334
+ chains into it.
335
+
336
+ The gate is three conditions, all required — `display-mode: standalone` (only
337
+ installed apps letterbox), `pointer: coarse` (keeps macOS Dock apps out), and
338
+ `@supports (-webkit-touch-callout: none)` (iOS/iPadOS WebKit only). Selectors
339
+ carry `html:root` (specificity 0,1,2) deliberately: app resets commonly declare
340
+ `body { overflow: hidden }` at (0,0,1) _after_ this sheet, and the unlock has to
341
+ win the cascade without `!important`.
342
+
343
+ Consumer obligations, both silent if missed:
344
+
345
+ - **Ship the unlock at first parse.** iOS samples at launch, and a bundled
346
+ stylesheet arrives after it. An app whose shell paints before its CSS bundle
347
+ must inline a copy of the unlock in the document head — junoui's copy in
348
+ `juno.css` is too late on its own.
349
+ - **Do not override `body::after`.** The spacer is `body::after`; a consumer
350
+ that needs that pseudo-element for itself must reproduce the spacer at the
351
+ same gate. None of junoui's own components use it.
352
+
353
+ ### How to know it is fixed upstream
354
+
355
+ In standalone, `window.innerHeight === screen.height` **and
356
+ `100lvh === 100dvh`**, for a document that **cannot** scroll — that is the case
357
+ that still misbehaves. When that holds, the unlock is dead weight and can be
358
+ removed. Until then it is harmless where it does not apply, because the gate
359
+ excludes every non-iOS and non-installed context.
360
+
361
+ - Sources: nexora `CLAUDE.md` §15, entry dated 2026-08-13; the four-mode testbed
362
+ `web/public/expansion-demo.html` (kept as a standing rig); 201 device readings
363
+ collected by `scripts/viewport_probe_collect.py`. Tracked upstream as ticket
364
+ 20260812-006, drafted for WebKit Bugzilla / Feedback Assistant and awaiting
365
+ filing. Related Apple Developer Forums threads: 800798, 798014 (iOS 26
366
+ safe-area insets wrong until a background/resume — the "corrects itself later"
367
+ shape matches exactly), 797124.
368
+
179
369
  ## Typography
180
370
 
181
371
  iOS: **17 pt default body size, 11 pt minimum**; Dynamic Type must accommodate
@@ -194,6 +384,14 @@ iOS Safari is widely observed to zoom the page onto a focused field under 16px.
194
384
  behavior, not published spec. The mitigation is harmless, so it stays — but do
195
385
  not cite it as documented, and re-verify it on iOS 26.
196
386
 
387
+ The floor is now a checked claim, not an asserted one:
388
+ `test/visual/tap-targets.spec.mjs` reads the computed `font-size` of a
389
+ `.juno-input` under the coarse-pointer Playwright project and asserts `>= 16`.
390
+ That check is also what found the floor had never worked — the rule sat in
391
+ `base.css`'s `@media (pointer: coarse)` block, where a media query adds no
392
+ specificity, so `components/input.css`'s own `.juno-input` font-size beat it on
393
+ source order. It now lives in `input.css`. Keep it there.
394
+
197
395
  ## Things a stylesheet controls that Apple says nothing about
198
396
 
199
397
  No primary Apple source was found for any of: `touch-action`,
@@ -214,7 +412,11 @@ One confirmed change raises the stakes: as of iOS/iPadOS 26, **every website
214
412
  added to the Home Screen opens as a web app by default** — "there are now zero
215
413
  requirements for 'installability'". junoui's CSS may therefore run in a
216
414
  standalone context, where `viewport-fit` and `env()` govern home-indicator and
217
- Dynamic Island clearance, for sites that never opted in.
415
+ Dynamic Island clearance, for sites that never opted in. The concrete
416
+ consequence is measured above in
417
+ [Home-Screen standalone: the letterbox](#home-screen-standalone-the-letterbox-and-why-basecss-unlocks-it) —
418
+ a site that never asked to be a web app now inherits both the letterbox and the
419
+ unlock.
218
420
 
219
421
  Unconfirmed leads, tracked in ticket 20260803-034: `vh` reportedly pinning to
220
422
  `window.outerHeight`; three new tab modes yielding different `innerHeight`; a
package/docs/layout.md CHANGED
@@ -269,3 +269,38 @@ You opt in per region; nothing forces a markup shape.
269
269
  These are all CSS. Behavior that needs state (resize observers feeding app state,
270
270
  virtualization, drag-resizable panels) belongs in your app or a sibling
271
271
  `junoui-<framework>` package — not the design system.
272
+
273
+ ## Floating-nav clearance
274
+
275
+ A scroller that a fixed dock or pillbar floats over reserves room at its foot so
276
+ the last row clears the overlay:
277
+
278
+ ```css
279
+ .my-scroller {
280
+ padding-block-end: var(--juno-dock-clearance);
281
+ }
282
+ ```
283
+
284
+ The value is **derived from the control it clears**, not a constant:
285
+
286
+ | Prop | Is |
287
+ | ----------------------------- | ------------------------------------------------------------------------------------------ |
288
+ | `--juno-dock-h` | the floating pill's laid-out height — bubble + item padding + pill padding + border |
289
+ | `--juno-pillbar-h` | the pillbar's, one size down (no bubble) |
290
+ | `--juno-dock-clearance-scale` | multiplier on that height, default `1` — see below |
291
+ | `--juno-dock-clearance` | `height × scale + space-16 (the bar's own margin) + space-8 + env(safe-area-inset-bottom)` |
292
+
293
+ **Why derived matters.** Set `--juno-size-tap-comfortable` at `:root` — which
294
+ junoui invites, and which is the whole point of a token — and the pill grows;
295
+ the clearance grows with it. The previous constant did not, and past a 58px
296
+ bubble it reserved less than the pill's own height plus its margin, so content
297
+ hid under the dock with nothing reporting it.
298
+
299
+ **The scale knob is for bars that shrink while scrolling.** Reserve at the SMALL
300
+ size by setting `--juno-dock-clearance-scale` to your compact factor. Reserving
301
+ at the live size means the reservation changes after the last scroll event, and
302
+ content relayouts under a finger that has already stopped moving.
303
+
304
+ **Custom properties resolve where they are declared.** Override
305
+ `--juno-size-tap-comfortable` at `:root` (or override `--juno-dock-h` directly);
306
+ overriding it on a subtree does not re-derive a token computed at `:root`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junoput01/junoui",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "junoui — a token-driven design system. Color carries semantic meaning, never decoration: every hue has one assigned role (NOMINAL / ACTIVE / TARGET / CAUTION / WARNING). Ships multi-platform tokens (CSS, SCSS, JS/TS, JSON, Android, iOS, Flutter) plus a framework-agnostic CSS component layer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -51,6 +51,7 @@
51
51
  "./icons": "./dist/icons/juno-icons.svg",
52
52
  "./subset": "./tools/subset-sprite.mjs",
53
53
  "./icons/inline": "./dist/icons/inline.js",
54
+ "./icons/install": "./dist/icons/install.js",
54
55
  "./package.json": "./package.json"
55
56
  },
56
57
  "files": [
@@ -80,6 +81,7 @@
80
81
  "showcase": "npm run build && node scripts/serve.mjs",
81
82
  "test:visual": "playwright test",
82
83
  "test:visual:update": "playwright test --update-snapshots",
84
+ "gate:consumer": "node scripts/consumer-gate.mjs",
83
85
  "changeset": "changeset",
84
86
  "version": "changeset version",
85
87
  "release": "npm run build && changeset publish"
@@ -94,5 +96,12 @@
94
96
  },
95
97
  "engines": {
96
98
  "node": ">=18"
97
- }
99
+ },
100
+ "browserslist": [
101
+ "iOS >= 17.0",
102
+ "Safari >= 17.0",
103
+ "Chrome >= 114",
104
+ "Edge >= 114",
105
+ "Firefox >= 125"
106
+ ]
98
107
  }
package/src/css/base.css CHANGED
@@ -19,13 +19,48 @@
19
19
  /* Floating-nav scroll clearance — a page/scroller that a fixed dock or
20
20
  pillbar floats over reserves this much room at its foot so the last row
21
21
  clears the overlay. Consumers write `padding-block-end:
22
- var(--juno-dock-clearance)` and stay correct when the dock geometry
23
- changes no hand-rolled calc() per app. Web-only (they carry env()), so
24
- they live in the CSS layer, not the cross-platform token set.
25
- --juno-dock-clearance pill height + bottom margin + breathing room;
26
- --juno-pillbar-clearance suits the shorter centered pill. */
27
- --juno-dock-clearance: calc(var(--juno-space-72) + var(--juno-space-20) + env(safe-area-inset-bottom, 0px));
28
- --juno-pillbar-clearance: calc(var(--juno-space-72) + env(safe-area-inset-bottom, 0px));
22
+ var(--juno-dock-clearance)`. Web-only (they carry env()), so they live in
23
+ the CSS layer, not the cross-platform token set.
24
+
25
+ DERIVED, not constant. These used to be `space-72 + space-20 + env()`,
26
+ which promised in this very comment to "stay correct when the dock
27
+ geometry changes" and could not: the floating pill measures 62px with the
28
+ default 44px bubble, and junoui explicitly invites a consumer to
29
+ parameterize that bubble (--juno-size-tap-comfortable). Past a 58px bubble
30
+ the old constant reserved LESS than the pill's own height plus its margin
31
+ — content hid under the dock, silently, on exactly the consumers who took
32
+ the invitation (20260815-026).
33
+
34
+ So the parts are named and the arithmetic is visible:
35
+ --juno-dock-h the pill's laid-out height: the bubble, the item's
36
+ padding-block, the pill's own padding, its border.
37
+ * -clearance-scale consumers that SHRINK the bar while scrolling
38
+ reserve at the small size, not the live one — a
39
+ reservation that tracks a value which changes after
40
+ the last scroll event relayouts content under a
41
+ finger that has already stopped moving. Default 1.
42
+ + space-16 + env() the pill's own bottom margin (dock.css), additive
43
+ because a floating element sits OFF the edge —
44
+ docs/ios-conformance.md, "max() vs addition".
45
+ + space-8 breathing room, so the last row is not flush. */
46
+ --juno-dock-h: calc(
47
+ var(--juno-size-tap-comfortable) + 4 * var(--juno-space-4) + 2 * var(--juno-border-width-1)
48
+ );
49
+ --juno-dock-clearance-scale: 1;
50
+ --juno-dock-clearance: calc(
51
+ var(--juno-dock-h) * var(--juno-dock-clearance-scale) + var(--juno-space-16) +
52
+ var(--juno-space-8) + env(safe-area-inset-bottom, 0px)
53
+ );
54
+
55
+ /* The pillbar is the same shape one size down: no bubble, so its height is
56
+ the tap target plus its own padding and border. */
57
+ --juno-pillbar-h: calc(
58
+ var(--juno-size-tap-comfortable) + 2 * var(--juno-space-4) + 2 * var(--juno-border-width-1)
59
+ );
60
+ --juno-pillbar-clearance: calc(
61
+ var(--juno-pillbar-h) * var(--juno-dock-clearance-scale) + var(--juno-space-16) +
62
+ var(--juno-space-8) + env(safe-area-inset-bottom, 0px)
63
+ );
29
64
 
30
65
  /* JS-readable motion contract. CSS-only `prefers-reduced-motion` never reaches
31
66
  imperative JS (scrollTo/scrollIntoView smooth behavior, rAF-driven transforms,
@@ -127,8 +162,11 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
127
162
  is harmless either way: hold text-entry controls (.juno-input covers input,
128
163
  textarea and the select's inner control) at a 16px floor on touch. max()
129
164
  keeps the floor even under a scaled-down --juno-font-scale, and still grows
130
- when scaled up. */
131
- .juno-input { font-size: max(16px, var(--juno-font-size-16)); }
165
+ when scaled up.
166
+ THE RULE ITSELF LIVES IN components/input.css, not here: a media query adds
167
+ no specificity, so this file's `.juno-input` would lose to input.css's own
168
+ `.juno-input` font-size later in the bundle. It did, silently, until
169
+ 20260815-006's coarse-pointer project measured it. */
132
170
 
133
171
  /* Kill the UA tap-highlight square on the interactive surfaces so it never
134
172
  flashes past a rounded control on tap. Consumers were adding this by hand
@@ -229,17 +267,11 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
229
267
  :root { --juno-border: var(--juno-border-strong); }
230
268
  }
231
269
 
232
- /* Windows High Contrast / forced-colors: opt into the system palette so
233
- borders and focus stay visible when our colors are overridden. */
234
- @media (forced-colors: active) {
235
- *:focus-visible { outline-color: Highlight; }
236
-
237
- .juno-badge,
238
- .juno-btn,
239
- .juno-card,
240
- .juno-readout { border: 1px solid CanvasText; }
241
- .juno-badge { forced-color-adjust: none; } /* keep status fill meaningful */
242
- }
270
+ /* Windows High Contrast / forced-colors: the opt-in lives in
271
+ overrides.css, NOT here. A media query adds no specificity, and this file is
272
+ bundled BEFORE components/, so `.juno-btn { border: }` in button.css beat
273
+ the forced-colors border every time — same 0,1,0 selector, later in the
274
+ bundle. See src/css/overrides.css and ticket 20260815-029. */
243
275
 
244
276
  /* ── iOS standalone letterbox unlock ─────────────────────────────────────────
245
277
  iOS WebKit (observed through 26.6, iPhone 16 Pro, iOS 18.7) sizes a
@@ -288,3 +320,30 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
288
320
  }
289
321
  }
290
322
  }
323
+
324
+ /* ── Popover API absent (Safari/iOS < 17.0) ──────────────────────────────────
325
+ junoui's overlay surfaces are built on the native Popover API. Without it
326
+ `popovertarget` does nothing, so they cannot open — an inconvenience. The
327
+ part that is a BUG is that the UA rule which hides a closed popover
328
+ (`[popover]:not(:popover-open) { display: none }`) does not exist either, so
329
+ the panel participates in layout: .juno-menu and .juno-popover are
330
+ `position: fixed` with `opacity: 0` and no pointer-events reset, i.e. a
331
+ 256-280px INVISIBLE panel parked at its static position, swallowing taps on
332
+ whatever it covers. (.juno-tooltip__bubble escapes it — it inherits
333
+ `pointer-events: none` from the CSS-only tooltip rule — and is listed anyway
334
+ so the set matches the docs.)
335
+
336
+ So: hide them. Absent beats invisibly-present. Apps detect the same condition
337
+ with CSS.supports('selector(:popover-open)') and render a fallback.
338
+
339
+ @supports selector() is itself Safari 14.1+; below that this condition is
340
+ invalid, the block is skipped, and behaviour is what it is today. Fail-open,
341
+ and far below any supported floor. Guards live here rather than per component
342
+ so the next popover-based component inherits it — see docs/browser-support.md
343
+ for the rule about when a feature gap earns a guard at all (functional
344
+ failures only; a missing animation does not get one). */
345
+ @supports not selector(:popover-open) {
346
+ .juno-menu[popover],
347
+ .juno-popover[popover],
348
+ .juno-tooltip__bubble[popover] { display: none; }
349
+ }
@@ -28,6 +28,18 @@
28
28
  color: var(--juno-data);
29
29
  }
30
30
 
31
+ /* The iOS focus-zoom floor, and it has to live HERE, not in base.css's
32
+ `@media (pointer: coarse)` block where it was written. A media query adds no
33
+ specificity, so `.juno-input { font-size: var(--juno-font-size-14) }` above —
34
+ same 0,1,0 selector, later in the bundle (base.css sorts before components/)
35
+ — simply won it, and the floor never applied on any touch device. Nothing
36
+ caught it because the suite had no coarse-pointer project until 20260815-006
37
+ added one; the numeric assertion in test/visual/tap-targets.spec.mjs is what
38
+ surfaced it. See base.css for the sourcing caveat on the behavior itself. */
39
+ @media (pointer: coarse) {
40
+ .juno-input { font-size: max(16px, var(--juno-font-size-16)); }
41
+ }
42
+
31
43
  .juno-input--sans { font-family: var(--juno-font-family-sans); }
32
44
 
33
45
  .juno-input::placeholder { color: var(--juno-muted); }
@@ -257,7 +257,7 @@
257
257
  <svg class="juno-icon" aria-hidden="true"><use href="…#juno-i-dots-three" /></svg>
258
258
  </button>
259
259
  <div class="juno-pillbar__tray">
260
- <div><!-- single wrapper, any element -->
260
+ <div>\3c !-- single wrapper, any element -->
261
261
  …the usual __item / __sep / __input children…
262
262
  </div>
263
263
  </div>
@@ -0,0 +1,51 @@
1
+ /* ════════════════════════════════════════════════════════════════════
2
+ * Overrides — cross-cutting gates, bundled LAST
3
+ * ════════════════════════════════════════════════════════════════════
4
+ * A @media or @supports block adds NO specificity. So a gate written in
5
+ * base.css — which the bundler emits before components/ — loses to any
6
+ * component rule declaring the same property on the same selector, purely
7
+ * on source order, and loses SILENTLY: the gate looks right in the file it
8
+ * was written in, and there is no error anywhere.
9
+ *
10
+ * Four instances of that shipped before this file existed (2026-08-15):
11
+ * - .juno-input's 16px iOS focus-zoom floor, beaten by input.css. The floor
12
+ * never applied on any touch device, in any release (20260815-011).
13
+ * - the forced-colors border below, beaten by button.css / badge.css /
14
+ * card.css (20260815-029).
15
+ * - the Popover fallback, which happens to win today only because nothing
16
+ * later declares `display` on those selectors — pinned by a test rather
17
+ * than by structure (20260815-013).
18
+ *
19
+ * So: gates whose whole job is to BEAT a component default live here, after
20
+ * everything they guard. Same reasoning the bundler already applies to
21
+ * utilities.css (role helpers must outrank component defaults) — see
22
+ * scripts/bundle-css.mjs.
23
+ *
24
+ * What does NOT belong here: a gate that only has to beat the UA or another
25
+ * rule in its own file. Keep those next to what they modify; this file is for
26
+ * the cross-file case, or it becomes a dumping ground and the cascade gets
27
+ * harder to read rather than easier.
28
+ *
29
+ * A build test (test/build.test.mjs) asserts the bundle contains no gated
30
+ * declaration that a later ungated rule overrides at equal-or-lower
31
+ * specificity. It asserts an EMPTY SET — no allowlist, because an allowlist
32
+ * is where violations go to be forgotten.
33
+ * ════════════════════════════════════════════════════════════════════ */
34
+
35
+ /* Windows High Contrast / forced-colors: opt into the system palette so
36
+ borders and focus stay visible when the user's palette replaces ours.
37
+
38
+ .juno-badge additionally sets `forced-color-adjust: none` to keep its status
39
+ fill meaningful — and that opt-out is why the badge was the one element where
40
+ the lost cascade actually reached the screen: it also disables the UA's own
41
+ repaint, which was silently rescuing .juno-btn and .juno-card. Measured, not
42
+ inferred (20260815-029). */
43
+ @media (forced-colors: active) {
44
+ *:focus-visible { outline-color: Highlight; }
45
+
46
+ .juno-badge,
47
+ .juno-btn,
48
+ .juno-card,
49
+ .juno-readout { border: 1px solid CanvasText; }
50
+ .juno-badge { forced-color-adjust: none; } /* keep status fill meaningful */
51
+ }