@seatlayer/js 0.51.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2114,6 +2114,13 @@ var BuyerAssetObjectUrls = class {
2114
2114
  }
2115
2115
  };
2116
2116
 
2117
+ // src/SeatPicker.ts
2118
+ import {
2119
+ createMinimapTransform,
2120
+ minimapPointToWorld,
2121
+ minimapViewportPlan
2122
+ } from "@seatlayer/core/picker/minimapGeometry";
2123
+
2117
2124
  // src/offerAvailability.ts
2118
2125
  var SALE_STATES = [
2119
2126
  "on-sale",
@@ -2252,6 +2259,12 @@ function tfStatic(key, fallback) {
2252
2259
  const v = t2(key);
2253
2260
  return v === key ? fallback : v;
2254
2261
  }
2262
+ var chartsLoadedThisPage = /* @__PURE__ */ new Set();
2263
+ function markChartLoaded(eventKey) {
2264
+ const seen = chartsLoadedThisPage.has(eventKey);
2265
+ chartsLoadedThisPage.add(eventKey);
2266
+ return seen;
2267
+ }
2255
2268
  function pointInPolygon(x, y, poly) {
2256
2269
  let inside = false;
2257
2270
  for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
@@ -2354,9 +2367,13 @@ var CSS = (
2354
2367
  the WIDGET's meanings, not an org's brand: premium gold, environment
2355
2368
  warning, danger, success. Hosts can still override them with CSS. */
2356
2369
  --sl-premium:#e8c15a;--sl-premium-deep:#c9a24b;--sl-premium-ink:#1c1608;
2357
- --sl-warn:#d8a425;--sl-danger:#e5484d;--sl-success:#22a06b}
2370
+ --sl-premium-text:color-mix(in srgb,var(--sl-premium-deep) 48%,var(--sl-text));
2371
+ --sl-warn:#d8a425;--sl-warn-text:color-mix(in srgb,var(--sl-warn) 48%,var(--sl-text));
2372
+ --sl-danger:#e5484d;--sl-success:#22a06b}
2358
2373
  .sl-picker *{box-sizing:border-box;margin:0;padding:0}
2359
- .sl-picker button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
2374
+ /* Keep the reset intentionally specificity-free. Standalone map and panel
2375
+ controls must be able to establish their own token surfaces in either theme. */
2376
+ .sl-picker :where(button){font:inherit;color:inherit;background:none;border:0;cursor:pointer}
2360
2377
 
2361
2378
  /* header */
2362
2379
  .sl-head{display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--sl-line);flex:none}
@@ -2552,11 +2569,11 @@ var CSS = (
2552
2569
  .sl-offer{display:none;margin:12px 14px 2px;padding:12px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));
2553
2570
  border-radius:12px;background:color-mix(in srgb,var(--sl-accent) 8%,var(--sl-surface));color:var(--sl-text)}
2554
2571
  .sl-offer.has{display:block}.sl-offer-main{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}
2555
- .sl-offer-copy{min-width:0}.sl-offer-kicker{display:block;font-size:10px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-accent)}
2572
+ .sl-offer-copy{min-width:0}.sl-offer-kicker{display:block;font-size:10px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-accent-text)}
2556
2573
  .sl-offer-name{display:block;margin-top:2px;font-size:13px;font-weight:800;line-height:1.3}.sl-offer-line{display:block;margin-top:3px;font-size:11px;line-height:1.35;color:var(--sl-muted)}
2557
2574
  .sl-offer-info{position:relative;flex:none}.sl-offer-info>summary{list-style:none;width:25px;height:25px;border:1px solid var(--sl-line);border-radius:999px;
2558
2575
  display:grid;place-items:center;cursor:pointer;font-size:12px;font-weight:850;color:var(--sl-text);background:var(--sl-surface)}
2559
- .sl-offer-info>summary::-webkit-details-marker{display:none}.sl-offer-info[open]>summary{border-color:var(--sl-accent);color:var(--sl-accent)}
2576
+ .sl-offer-info>summary::-webkit-details-marker{display:none}.sl-offer-info[open]>summary{border-color:var(--sl-accent);color:var(--sl-accent-text)}
2560
2577
  .sl-offer-detail{margin-top:10px;padding-top:9px;border-top:1px solid var(--sl-line);font-size:10.5px;line-height:1.45;color:var(--sl-muted)}
2561
2578
  .sl-sec{padding:14px 14px 4px;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:700}
2562
2579
  .sl-prices-sec{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:13px}
@@ -2574,8 +2591,8 @@ var CSS = (
2574
2591
  padding:0 6px;margin:0 -6px;border-radius:8px;cursor:pointer;transition:background .15s}
2575
2592
  .sl-price-row:hover,.sl-price-row:focus-visible{background:color-mix(in srgb,var(--sl-line) 40%,transparent)}
2576
2593
  .sl-price-row.sl-active{background:color-mix(in srgb,var(--sl-accent) 9%,transparent)}
2577
- .sl-price-was{margin-left:auto;color:var(--sl-muted);font-size:10px;text-decoration:line-through}.sl-price-offer{display:block;color:var(--sl-accent);font-size:10px;font-weight:750}
2578
- .sl-price-row.sl-active .sl-price-label{color:var(--sl-accent)}
2594
+ .sl-price-was{margin-left:auto;color:var(--sl-muted);font-size:10px;text-decoration:line-through}.sl-price-offer{display:block;color:var(--sl-accent-text);font-size:10px;font-weight:750}
2595
+ .sl-price-row.sl-active .sl-price-label{color:var(--sl-accent-text)}
2579
2596
  .sl-dot{width:9px;height:9px;border-radius:50%;flex:none}
2580
2597
  .sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
2581
2598
  .sl-price-left{font-size:11px;color:var(--sl-muted);font-variant-numeric:tabular-nums}
@@ -2610,7 +2627,7 @@ var CSS = (
2610
2627
  box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
2611
2628
  .sl-ticket-state{width:17px;height:17px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
2612
2629
  background:var(--sl-accent);color:var(--sl-accent-ink)}
2613
- .sl-ticket-state.held{background:color-mix(in srgb,var(--sl-accent) 18%,var(--sl-surface));color:var(--sl-accent)}
2630
+ .sl-ticket-state.held{background:color-mix(in srgb,var(--sl-accent) 18%,var(--sl-surface));color:var(--sl-accent-text)}
2614
2631
  .sl-ticket-state svg{width:10px;height:10px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
2615
2632
  .sl-chip-main{min-width:0;padding:8px 10px 8px 11px;display:flex;flex-direction:column;justify-content:center;gap:5px}
2616
2633
  .sl-chip-id{display:flex;gap:12px;min-width:0}
@@ -2653,7 +2670,7 @@ var CSS = (
2653
2670
  .sl-ga-qty span{min-width:16px;text-align:center;font-weight:800;font-variant-numeric:tabular-nums}
2654
2671
 
2655
2672
  /* footer */
2656
- .sl-foot{position:relative;z-index:2;padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none;
2673
+ .sl-foot{position:relative;z-index:2;padding:10px 16px 8px;border-top:1px solid var(--sl-line);flex:none;
2657
2674
  background:var(--sl-bg);box-shadow:0 -10px 24px -22px rgba(0,0,0,.72)}
2658
2675
  .sl-hold-note{display:none;align-items:center;gap:7px;margin-bottom:8px;padding:7px 8px;border-radius:var(--sl-r-sm);
2659
2676
  border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
@@ -2667,14 +2684,13 @@ var CSS = (
2667
2684
  .sl-hold-note>span{flex:1;min-width:0}
2668
2685
  .sl-hold-change{flex:none;min-height:30px;padding:5px 8px;border-radius:8px;border:1px solid var(--sl-line);
2669
2686
  color:var(--sl-text);font-size:10.5px;font-weight:750;white-space:nowrap}
2670
- .sl-hold-change:hover,.sl-hold-change:focus-visible{border-color:var(--sl-accent);color:var(--sl-accent)}
2687
+ .sl-hold-change:hover,.sl-hold-change:focus-visible{border-color:var(--sl-accent);color:var(--sl-accent-text)}
2671
2688
  .sl-hold-change:disabled{opacity:.58;cursor:wait}
2672
2689
  .sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
2673
2690
  .sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
2674
2691
  .sl-value-pop{animation:slValuePop .32s cubic-bezier(.2,.8,.2,1)}
2675
- /* Primary checkout CTA. Scoped under .sl-picker so it OUTWEIGHS the
2676
- '.sl-picker button' reset (0,1,1) \u2014 an unscoped '.sl-cta' (0,1,0) loses to it
2677
- and the button renders as plain text with no accent fill. */
2692
+ /* Primary checkout CTA. The extra root scope documents that this is widget
2693
+ chrome and keeps the contract resilient when embedded beside host styles. */
2678
2694
  .sl-picker .sl-cta{display:flex;align-items:center;justify-content:center;width:100%;min-height:44px;
2679
2695
  padding:12px 16px;border-radius:var(--sl-r-sm);font-weight:800;font-size:14px;line-height:1.1;
2680
2696
  background:var(--sl-accent);color:var(--sl-accent-ink);
@@ -2685,8 +2701,8 @@ var CSS = (
2685
2701
  .sl-cta-spin,.sl-ba-spin{width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;
2686
2702
  animation:slspin .7s linear infinite;flex:none}
2687
2703
  /* Disabled ("Select seats"): quieter, but still a full-width button shape. */
2688
- .sl-picker .sl-cta:disabled{background:var(--sl-surface);color:var(--sl-muted);opacity:1;
2689
- cursor:not-allowed;filter:none;transform:none}
2704
+ .sl-picker .sl-cta:disabled{background:color-mix(in srgb,var(--sl-bg) 35%,var(--sl-surface));color:var(--sl-muted);
2705
+ box-shadow:inset 0 0 0 1px var(--sl-line);opacity:1;cursor:not-allowed;filter:none;transform:none}
2690
2706
 
2691
2707
  /* Chrome anchor regions (Feature 6) \u2014 every interactive map overlay is APPENDED
2692
2708
  INTO one of these positioned flex containers and flows/stacks within it, so no
@@ -2716,7 +2732,7 @@ var CSS = (
2716
2732
  in the same region, so no displacement hack is needed. */
2717
2733
  .sl-testbadge{display:inline-flex;align-items:center;padding:5px 10px;border-radius:7px;pointer-events:none;
2718
2734
  font-size:10px;font-weight:850;letter-spacing:.13em;line-height:1.2;text-transform:uppercase;white-space:nowrap;
2719
- color:var(--sl-warn);background:color-mix(in srgb,var(--sl-warn) 13%,var(--sl-surface));
2735
+ color:var(--sl-warn-text);background:color-mix(in srgb,var(--sl-warn) 13%,var(--sl-surface));
2720
2736
  border:1px dashed color-mix(in srgb,var(--sl-warn) 55%,transparent)}
2721
2737
 
2722
2738
  /* zoom column (flows within the bottom-right region) */
@@ -2783,7 +2799,7 @@ var CSS = (
2783
2799
  .sl-extend-btn{flex:none;padding:8px 14px;border-radius:999px;font-weight:800;font-size:12.5px;
2784
2800
  background:var(--sl-accent);color:var(--sl-accent-ink);transition:filter .15s,opacity .15s}
2785
2801
  .sl-extend-btn:hover{filter:brightness(1.08)}
2786
- .sl-extend-btn:disabled{opacity:.5;cursor:not-allowed}
2802
+ .sl-extend-btn:disabled{opacity:.62;cursor:not-allowed}
2787
2803
 
2788
2804
  /* booked confirmation overlay (covers the widget once the held seats are sold) */
2789
2805
  .sl-booked{position:absolute;inset:0;z-index:11;display:flex;flex-direction:column;align-items:center;
@@ -2810,7 +2826,7 @@ var CSS = (
2810
2826
  justify-content:center;text-align:center;gap:8px;padding:24px;
2811
2827
  background:color-mix(in srgb,var(--sl-bg) 82%,transparent);backdrop-filter:blur(4px)}
2812
2828
  .sl-soldout.on{display:flex}
2813
- .sl-soldout-eyebrow{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--sl-accent);font-weight:800}
2829
+ .sl-soldout-eyebrow{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--sl-accent-text);font-weight:800}
2814
2830
  .sl-soldout-title{font-size:32px;font-weight:800;color:var(--sl-text);line-height:1.05}
2815
2831
  .sl-soldout-copy{max-width:360px;font-size:13px;color:var(--sl-muted);line-height:1.5}
2816
2832
 
@@ -2826,9 +2842,9 @@ var CSS = (
2826
2842
  /* "Powered by SeatLayer" attribution badge (side-panel foot) \u2014 the canonical
2827
2843
  Layered Rows mark + wordmark. Hidden when the host opts out or the org's paid
2828
2844
  theme sets hideBadge. */
2829
- .sl-powered{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:10px;
2830
- font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--sl-text);opacity:.72;
2831
- text-decoration:none;padding:6px 10px;border-radius:999px;width:fit-content;margin-inline:auto;
2845
+ .sl-powered{display:flex;align-items:center;justify-content:center;gap:5px;margin-top:2px;
2846
+ font-size:11px;font-weight:600;letter-spacing:.02em;color:var(--sl-text);opacity:.72;
2847
+ text-decoration:none;padding:2px 8px;border-radius:999px;width:fit-content;margin-inline:auto;
2832
2848
  transition:opacity .15s ease,background-color .15s ease}
2833
2849
  .sl-powered:hover{opacity:1;background:color-mix(in srgb,var(--sl-text) 8%,transparent)}
2834
2850
  .sl-powered:focus-visible{opacity:1;outline:2px solid var(--sl-accent);outline-offset:2px}
@@ -2866,10 +2882,22 @@ var CSS = (
2866
2882
  .sl-confirm-field:first-child .sl-confirm-value{font-size:13.5px;line-height:1.25;white-space:normal;
2867
2883
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
2868
2884
  .sl-confirm-cat{display:flex;align-items:center;gap:8px;padding:10px 12px;background:color-mix(in srgb,var(--sl-cat) 76%,var(--sl-surface))}
2869
- .sl-confirm-cat .sl-dot{border:2px solid rgba(255,255,255,.78);width:11px;height:11px}
2870
- .sl-confirm-cat-name{font-size:13.5px;font-weight:800;color:#fff;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2871
- .sl-confirm-price{font-size:17px;font-weight:850;color:#fff;font-variant-numeric:tabular-nums}
2885
+ .sl-confirm-cat .sl-dot{border:2px solid color-mix(in srgb,var(--sl-cat-ink) 78%,transparent);width:11px;height:11px}
2886
+ .sl-confirm-cat-name{font-size:13.5px;font-weight:800;color:var(--sl-cat-ink);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2887
+ .sl-confirm-price{font-size:17px;font-weight:850;color:var(--sl-cat-ink);font-variant-numeric:tabular-nums}
2872
2888
  .sl-confirm-body{padding:11px 12px 12px}
2889
+ .sl-confirm-tiers{display:grid;gap:6px;margin:0 0 9px;padding:0;border:0}
2890
+ .sl-confirm-tiers legend{margin:0 0 6px;padding:0;color:var(--sl-muted);font-size:10px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}
2891
+ .sl-confirm-tier{display:flex;align-items:center;gap:9px;width:100%;min-height:40px;padding:8px 10px;border:1px solid var(--sl-line);
2892
+ border-radius:9px;color:var(--sl-text);background:color-mix(in srgb,var(--sl-bg) 72%,var(--sl-surface));text-align:left}
2893
+ .sl-confirm-tier:hover{border-color:color-mix(in srgb,var(--sl-accent) 58%,var(--sl-line))}
2894
+ .sl-confirm-tier[aria-pressed="true"]{border-color:var(--sl-accent);background:color-mix(in srgb,var(--sl-accent) 13%,var(--sl-surface));
2895
+ box-shadow:inset 3px 0 0 var(--sl-accent)}
2896
+ .sl-confirm-tier-copy{min-width:0;flex:1;display:grid;gap:2px}
2897
+ .sl-confirm-tier-name{min-width:0;font-size:12.5px;font-weight:800}
2898
+ .sl-confirm-tier-note,.sl-tier-note{color:var(--sl-muted);font-size:10.5px;line-height:1.3}
2899
+ .sl-confirm-tier[aria-pressed="true"] .sl-confirm-tier-note{color:var(--sl-text)}
2900
+ .sl-confirm-tier-price{font-size:13px;font-weight:850;font-variant-numeric:tabular-nums}
2873
2901
  .sl-confirm-row{display:flex;gap:8px;margin-top:10px}
2874
2902
  .sl-confirm-row button{flex:1;min-height:44px;padding:9px 12px;border-radius:9px;font-weight:800;font-size:13px}
2875
2903
  .sl-confirm-add{background:var(--sl-accent)!important;color:var(--sl-accent-ink)!important;display:flex;align-items:center;justify-content:center;gap:7px}
@@ -2887,6 +2915,7 @@ var CSS = (
2887
2915
  .sl-picker[data-layout="narrow"] .sl-confirm-cat-name{font-size:12.5px}
2888
2916
  .sl-picker[data-layout="narrow"] .sl-confirm-price{font-size:15px}
2889
2917
  .sl-picker[data-layout="narrow"] .sl-confirm-body{padding:9px 10px 10px}
2918
+ .sl-picker[data-layout="narrow"] .sl-confirm-tier{min-height:38px;padding:7px 9px}
2890
2919
  .sl-picker[data-layout="narrow"] .sl-confirm-thumbwrap{height:62px;margin-bottom:6px}
2891
2920
  .sl-picker[data-layout="narrow"] .sl-confirm-3d{margin-top:7px;padding:6px}
2892
2921
  .sl-picker[data-layout="narrow"] .sl-confirm-row{margin-top:8px}
@@ -2910,7 +2939,7 @@ var CSS = (
2910
2939
  .sl-table-stepper{display:grid;grid-template-columns:48px 1fr 48px;align-items:center;border:1px solid var(--sl-line);
2911
2940
  border-radius:12px;overflow:hidden;background:var(--sl-bg)}
2912
2941
  .sl-table-stepper button{height:48px;font-size:24px;font-weight:700;background:color-mix(in srgb,var(--sl-line) 34%,transparent)!important}
2913
- .sl-table-stepper button:disabled{opacity:.38;cursor:not-allowed}
2942
+ .sl-table-stepper button:disabled{opacity:.58;cursor:not-allowed}
2914
2943
  .sl-table-stepper output{text-align:center;font-size:19px;font-weight:850;font-variant-numeric:tabular-nums}
2915
2944
  .sl-table-range{margin-top:7px;color:var(--sl-muted);font-size:11px;text-align:center}
2916
2945
  .sl-table-actions{display:flex;gap:9px;margin-top:17px}.sl-table-actions button{flex:1;min-height:46px;border-radius:10px;font-weight:800}
@@ -2952,14 +2981,14 @@ var CSS = (
2952
2981
  .sl-ba::after{content:'\u2726';position:absolute;right:10px;top:3px;color:color-mix(in srgb,var(--sl-accent) 20%,transparent);font-size:42px;line-height:1}
2953
2982
  .sl-ba-title,.sl-ba-copy,.sl-ba select,.sl-ba-qty,.sl-ba-go{position:relative;z-index:1}
2954
2983
  .sl-ba-title{grid-column:1/-1;display:flex;align-items:center;gap:7px;font-size:13px;font-weight:850}
2955
- .sl-ba-title .spark{color:var(--sl-accent);font-size:16px}
2984
+ .sl-ba-title .spark{color:var(--sl-accent-text);font-size:16px}
2956
2985
  .sl-ba-copy{grid-column:1/-1;margin:-4px 0 2px 23px;color:var(--sl-muted);font-size:10.5px;line-height:1.35}
2957
2986
  .sl-ba-copy .narrow{display:none}
2958
2987
  /* "\u2605 Best seats" premium quick-pick \u2014 gold accent echoing the \u2605 Premium pill on
2959
2988
  the confirm popover; deliberately distinct from the accent-toned qty/go. */
2960
2989
  .sl-ba-premium{position:relative;z-index:1;grid-column:1/-1;justify-self:start;display:inline-flex;align-items:center;gap:6px;
2961
2990
  padding:6px 12px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.02em;cursor:pointer;
2962
- color:var(--sl-premium-deep);background:color-mix(in srgb,var(--sl-premium) 10%,var(--sl-surface));
2991
+ color:var(--sl-premium-text);background:color-mix(in srgb,var(--sl-premium) 10%,var(--sl-surface));
2963
2992
  border:1px solid color-mix(in srgb,var(--sl-premium) 34%,var(--sl-line));transition:filter .15s,background .15s,color .15s}
2964
2993
  .sl-ba-premium .star{font-size:12px;line-height:1;color:var(--sl-premium)}
2965
2994
  .sl-ba-premium:hover{filter:brightness(1.05)}
@@ -3012,7 +3041,7 @@ var CSS = (
3012
3041
  back out of a reopened card. */
3013
3042
  .sl-ba-reopen{align-self:flex-start;display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:5px 12px;margin-top:2px;
3014
3043
  border:1px dashed color-mix(in srgb,var(--sl-accent) 40%,var(--sl-line));border-radius:999px;
3015
- color:var(--sl-accent);font-size:11.5px;font-weight:750;transition:border-color .15s,background .15s}
3044
+ color:var(--sl-accent-text);font-size:11.5px;font-weight:750;transition:border-color .15s,background .15s}
3016
3045
  .sl-ba-reopen:hover,.sl-ba-reopen:focus-visible{background:color-mix(in srgb,var(--sl-accent) 8%,transparent)}
3017
3046
  .sl-ba-back{grid-column:1/-1;justify-self:start;min-height:30px;padding:3px 0;color:var(--sl-muted);
3018
3047
  font-size:11px;font-weight:700;text-decoration:underline;text-underline-offset:3px;transition:color .15s}
@@ -3134,7 +3163,7 @@ var CSS = (
3134
3163
  border:1px solid rgba(150,165,205,.35);backdrop-filter:blur(6px);cursor:pointer}
3135
3164
  .sl-view3d-nav button:hover,.sl-view3d-nav button:focus-visible{color:#eef1f8;border-color:rgba(190,205,240,.6)}
3136
3165
  .sl-view3d-nav button[aria-pressed="true"]{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
3137
- .sl-view3d-nav button:disabled{opacity:.45;cursor:not-allowed}
3166
+ .sl-view3d-nav button:disabled{opacity:.6;cursor:not-allowed}
3138
3167
  .sl-view3d-nav-toggle{display:none!important}
3139
3168
  .sl-view3d-nav select{min-height:38px;max-width:100%;padding:7px 34px 7px 12px;
3140
3169
  border-radius:999px;font:700 11.5px/1 inherit;color:#eef1f8;background:rgba(12,18,32,.86);
@@ -3182,7 +3211,7 @@ var CSS = (
3182
3211
  .sl-confirm-confidence strong,.sl-confirm-confidence small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3183
3212
  .sl-confirm-confidence strong{font-size:11px}.sl-confirm-confidence small{margin-top:2px;color:var(--sl-muted);font-size:9.5px}
3184
3213
  .sl-confirm-confidence em{display:none;font-style:normal}
3185
- .sl-confirm-confidence>b{flex:none;font-size:11px;color:var(--sl-accent)}
3214
+ .sl-confirm-confidence>b{flex:none;font-size:11px;color:var(--sl-accent-text)}
3186
3215
  .sl-confirm-confidence:hover,.sl-confirm-confidence:focus-visible{border-color:var(--sl-accent)}
3187
3216
  /* The production 3D decision dock is deliberately denser than the 2D popup:
3188
3217
  the venue remains the main content and the two inspection actions share one
@@ -3264,7 +3293,7 @@ var CSS = (
3264
3293
  .sl-view3d-compare-actions{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;margin-top:12px}
3265
3294
  .sl-view3d-compare-actions button{min-height:44px;border-radius:9px;border:1px solid var(--sl-line);font-size:12px;font-weight:800}
3266
3295
  .sl-view3d-compare-actions .select{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
3267
- .sl-view3d-compare-actions button:disabled{opacity:.5;cursor:not-allowed}
3296
+ .sl-view3d-compare-actions button:disabled{opacity:.62;cursor:not-allowed}
3268
3297
  .sl-view3d-passport-shell{position:absolute;inset:0;z-index:40;display:grid;place-items:center;padding:16px}
3269
3298
  .sl-view3d-passport-scrim{position:absolute;inset:0;background:rgba(3,6,12,.8);backdrop-filter:blur(6px)}
3270
3299
  .sl-view3d-passport{position:relative;width:min(540px,100%);max-height:min(700px,calc(100% - 20px));overflow:auto;
@@ -3305,14 +3334,17 @@ var CSS = (
3305
3334
  .sl-confirm-3d:hover,.sl-confirm-3d:focus-visible{border-color:var(--sl-accent);background:color-mix(in srgb,var(--sl-accent) 20%,transparent)}
3306
3335
  .sl-confirm-3d svg{width:15px;height:15px;stroke:currentColor;stroke-width:1.9;fill:none;stroke-linecap:round;stroke-linejoin:round}
3307
3336
 
3308
- /* multi-floor switcher (flows within the left-rail region) */
3309
- .sl-floors{display:none;flex-direction:column;gap:6px;max-width:100%}
3337
+ /* One compact multi-floor selector (flows within the left-rail region). */
3338
+ .sl-floors{display:none;align-items:center;gap:6px;max-width:min(260px,100%);padding:5px;border-radius:999px;
3339
+ background:color-mix(in srgb,var(--sl-surface) 92%,transparent);border:1px solid var(--sl-line);
3340
+ box-shadow:var(--sl-shadow);backdrop-filter:blur(8px)}
3310
3341
  .sl-floors.on{display:flex}
3311
- .sl-floors button{padding:7px 13px;border-radius:999px;font-size:12px;font-weight:700;background:var(--sl-surface);
3312
- border:1px solid var(--sl-line);color:var(--sl-muted);white-space:nowrap;max-width:100%;overflow:hidden;
3313
- text-overflow:ellipsis;transition:color .15s,border-color .15s}
3314
- .sl-floors button:hover{color:var(--sl-text)}
3315
- .sl-floors button.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
3342
+ .sl-floors select{min-width:0;max-width:210px;min-height:34px;padding:6px 30px 6px 10px;border:0;border-radius:999px;
3343
+ font:700 12px/1 inherit;color:var(--sl-text);background:var(--sl-surface);cursor:pointer}
3344
+ .sl-floors select:focus-visible,.sl-floor-info:focus-visible{outline:2px solid var(--sl-accent);outline-offset:2px}
3345
+ .sl-floor-info{display:inline-grid;place-items:center;flex:0 0 30px;width:30px;height:30px;border-radius:999px;
3346
+ border:1px solid var(--sl-line);color:var(--sl-muted);background:transparent;font:800 13px/1 inherit;cursor:help}
3347
+ .sl-floor-info:hover{color:var(--sl-text);border-color:var(--sl-accent)}
3316
3348
 
3317
3349
  /* tapped-section summary card \u2014 docks INSIDE the top-center anchor region on
3318
3350
  wide (flows below the rung pills, never over them, never floating over the
@@ -3347,8 +3379,14 @@ var CSS = (
3347
3379
  .sl-seccard-mix-dot{width:8px;height:8px;border-radius:50%;flex:none}
3348
3380
  .sl-seccard-mix-price{font-weight:700;color:var(--sl-text)}
3349
3381
  .sl-seccard-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px}
3350
- .sl-seccard-overview{font-size:12px;font-weight:800;color:var(--sl-accent)}
3382
+ .sl-seccard-overview{font-size:12px;font-weight:800;color:var(--sl-accent-text)}
3351
3383
  .sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
3384
+ .sl-seccard-nav{display:inline-flex;align-items:center;gap:4px;margin-left:auto}
3385
+ .sl-seccard-nav button{width:28px;height:28px;border:1px solid var(--sl-line);border-radius:999px;color:var(--sl-text);
3386
+ background:color-mix(in srgb,var(--sl-surface) 88%,var(--sl-bg));font-size:14px;font-weight:850;line-height:1}
3387
+ .sl-seccard-nav button:hover,.sl-seccard-nav button:focus-visible{border-color:var(--sl-accent);color:var(--sl-accent-text)}
3388
+ .sl-seccard.mini .sl-seccard-nav,.sl-seccard.strip .sl-seccard-nav{margin-left:2px}
3389
+ .sl-seccard.mini .sl-seccard-nav button,.sl-seccard.strip .sl-seccard-nav button{width:24px;height:24px;font-size:12px}
3352
3390
 
3353
3391
  /* view-from-seat button on the confirm popover */
3354
3392
  /* Eager sightline preview inside the confirm card */
@@ -3401,10 +3439,29 @@ var CSS = (
3401
3439
  white-space:nowrap;pointer-events:none;max-width:90%;overflow:hidden;text-overflow:ellipsis}
3402
3440
 
3403
3441
  /* F3 minimap \u2014 venue overview + live viewport rect (flows in the bottom-left region) */
3404
- .sl-minimap{border:1px solid var(--sl-line);border-radius:9px;
3405
- overflow:hidden;background:var(--sl-surface);box-shadow:0 12px 34px -14px rgba(0,0,0,.55);line-height:0;cursor:pointer}
3442
+ .sl-minimap{border:1px solid var(--sl-line);border-radius:9px;overflow:hidden;background:var(--sl-surface);
3443
+ box-shadow:0 12px 34px -14px rgba(0,0,0,.55);line-height:0;touch-action:none}
3444
+ .sl-minimap-bar{min-height:24px;padding:3px 5px 3px 8px;display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--sl-line);
3445
+ color:var(--sl-muted);font-size:9px;font-weight:800;line-height:1.2;letter-spacing:.06em;text-transform:uppercase}
3446
+ .sl-minimap-label{min-width:0;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
3447
+ .sl-minimap-key{display:inline-flex;align-items:center;gap:3px;flex:none}
3448
+ .sl-minimap-key i{width:6px;height:6px;border-radius:999px;background:var(--sl-success)}
3449
+ .sl-minimap-key i.selected{background:var(--sl-accent)}
3450
+ .sl-minimap-key i.inactive{background:var(--sl-muted);opacity:.45}
3451
+ .sl-minimap-key i.focused{background:transparent;border:1.5px solid var(--sl-accent);width:7px;height:7px}
3452
+ .sl-minimap-close{display:none;width:24px;height:24px;border-radius:6px;color:var(--sl-muted);font-size:15px;line-height:1}
3406
3453
  .sl-minimap canvas{display:block}
3454
+ .sl-minimap canvas:focus-visible{outline:2px solid var(--sl-accent);outline-offset:-3px}
3455
+ .sl-minimap canvas.sl-dragging{cursor:grabbing}
3456
+ .sl-minimap-toggle{display:none;width:42px;height:42px;border:1px solid var(--sl-line);border-radius:10px;background:var(--sl-surface);
3457
+ color:var(--sl-text);box-shadow:0 10px 28px -14px rgba(0,0,0,.65);align-items:center;justify-content:center}
3458
+ .sl-minimap-toggle svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linejoin:round}
3407
3459
  .sl-picker[data-layout="narrow"] .sl-minimap{display:none}
3460
+ .sl-picker[data-layout="narrow"] .sl-minimap-toggle{display:flex}
3461
+ .sl-picker[data-layout="narrow"][data-minimap-open="true"] .sl-minimap{display:block}
3462
+ .sl-picker[data-layout="narrow"][data-minimap-open="true"] .sl-minimap-toggle{display:none}
3463
+ .sl-picker[data-layout="narrow"] .sl-minimap canvas{width:min(168px,42vw)!important;height:auto!important}
3464
+ .sl-picker[data-layout="narrow"] .sl-minimap-close{display:grid;place-items:center}
3408
3465
 
3409
3466
  /* F4 legend reflection: rows + counts for out-of-band categories read muted */
3410
3467
  /* Out-of-band rows are exactly the rows a buyer reads while deciding whether
@@ -3482,17 +3539,88 @@ function formatWhen(startsAt, timezone, locale) {
3482
3539
  }
3483
3540
  return at.toLocaleString(locale, options);
3484
3541
  }
3485
- function resolveTokens(chart, host) {
3542
+ function parseThemeColor(value) {
3543
+ if (!value) return null;
3544
+ const color = value.trim().toLowerCase();
3545
+ if (color === "white") return { r: 255, g: 255, b: 255 };
3546
+ if (color === "black") return { r: 0, g: 0, b: 0 };
3547
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(color)?.[1];
3548
+ if (hex) {
3549
+ const expanded = hex.length <= 4 ? [...hex].map((digit) => `${digit}${digit}`).join("") : hex;
3550
+ return {
3551
+ r: Number.parseInt(expanded.slice(0, 2), 16),
3552
+ g: Number.parseInt(expanded.slice(2, 4), 16),
3553
+ b: Number.parseInt(expanded.slice(4, 6), 16)
3554
+ };
3555
+ }
3556
+ const rgb = /^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*[\d.]+%?)?\s*\)$/i.exec(color);
3557
+ if (!rgb) return null;
3558
+ return {
3559
+ r: Math.max(0, Math.min(255, Number(rgb[1]))),
3560
+ g: Math.max(0, Math.min(255, Number(rgb[2]))),
3561
+ b: Math.max(0, Math.min(255, Number(rgb[3])))
3562
+ };
3563
+ }
3564
+ function relativeLuminance(color) {
3565
+ const channel = (value) => {
3566
+ const normalized = value / 255;
3567
+ return normalized <= 0.04045 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
3568
+ };
3569
+ return 0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b);
3570
+ }
3571
+ function themeContrast(foreground, background) {
3572
+ const fg = parseThemeColor(foreground);
3573
+ const bg = parseThemeColor(background);
3574
+ if (!fg || !bg) return null;
3575
+ const lighter = Math.max(relativeLuminance(fg), relativeLuminance(bg));
3576
+ const darker = Math.min(relativeLuminance(fg), relativeLuminance(bg));
3577
+ return (lighter + 0.05) / (darker + 0.05);
3578
+ }
3579
+ function highestContrastInk(background, dark = "#172033", light = "#eef1f8") {
3580
+ return (themeContrast(dark, background) ?? 0) >= (themeContrast(light, background) ?? 0) ? dark : light;
3581
+ }
3582
+ function rgbHex(color) {
3583
+ const channel = (value) => Math.round(value).toString(16).padStart(2, "0");
3584
+ return `#${channel(color.r)}${channel(color.g)}${channel(color.b)}`;
3585
+ }
3586
+ function mixThemeColors(first, second, firstWeight) {
3587
+ const a = parseThemeColor(first);
3588
+ const b = parseThemeColor(second);
3589
+ if (!a || !b) return null;
3590
+ const weight = Math.max(0, Math.min(1, firstWeight));
3591
+ return rgbHex({
3592
+ r: a.r * weight + b.r * (1 - weight),
3593
+ g: a.g * weight + b.g * (1 - weight),
3594
+ b: a.b * weight + b.b * (1 - weight)
3595
+ });
3596
+ }
3597
+ function readableAccentText(accent, text, background, surface) {
3598
+ if ((themeContrast(accent, background) ?? 4.5) >= 4.5 && (themeContrast(accent, surface) ?? 4.5) >= 4.5) return accent;
3599
+ if (!parseThemeColor(accent) || !parseThemeColor(text)) return text;
3600
+ for (let textWeight = 0.15; textWeight <= 1; textWeight += 0.05) {
3601
+ const candidate = mixThemeColors(accent, text, 1 - textWeight);
3602
+ if (candidate && (themeContrast(candidate, background) ?? 0) >= 4.5 && (themeContrast(candidate, surface) ?? 0) >= 4.5) return candidate;
3603
+ }
3604
+ return text;
3605
+ }
3606
+ function resolvePickerTokens(chart, host) {
3607
+ const background = host?.background ?? chart?.background ?? "#0f1522";
3608
+ const isLight = parseThemeColor(background) ? highestContrastInk(background) === "#172033" : false;
3609
+ const surface = host?.surface ?? (isLight ? "#ffffff" : "#1a2234");
3486
3610
  const accent = host?.accent ?? chart?.accent ?? "#f4b740";
3487
- const accentInk = host?.accentInk ?? chart?.accentInk ?? "#1a1200";
3611
+ const chartAccentInk = chart?.accentInk;
3612
+ const accentInk = host?.accentInk ?? (chartAccentInk && (themeContrast(chartAccentInk, accent) ?? 4.5) >= 4.5 ? chartAccentInk : highestContrastInk(accent, "#1a1200", "#ffffff"));
3613
+ const chartText = chart?.textColor;
3614
+ const text = host?.text ?? (chartText && (themeContrast(chartText, background) ?? 4.5) >= 4.5 && (themeContrast(chartText, surface) ?? 4.5) >= 4.5 ? chartText : highestContrastInk(background));
3488
3615
  return {
3489
3616
  "--sl-accent": accent,
3490
3617
  "--sl-accent-ink": accentInk,
3491
- "--sl-bg": host?.background ?? chart?.background ?? "#0f1522",
3492
- "--sl-surface": host?.surface ?? "#1a2234",
3493
- "--sl-text": host?.text ?? chart?.textColor ?? "#eef1f8",
3494
- "--sl-muted": host?.muted ?? "#8b93a7",
3495
- "--sl-line": host?.line ?? "rgba(139,147,167,.22)",
3618
+ "--sl-accent-text": readableAccentText(accent, text, background, surface),
3619
+ "--sl-bg": background,
3620
+ "--sl-surface": surface,
3621
+ "--sl-text": text,
3622
+ "--sl-muted": host?.muted ?? (isLight ? "#667085" : "#a5aec2"),
3623
+ "--sl-line": host?.line ?? (isLight ? "rgba(23,32,51,.16)" : "rgba(165,174,194,.24)"),
3496
3624
  "--sl-font": host?.fontFamily ?? chart?.fontFamily ?? "-apple-system,BlinkMacSystemFont,'Segoe UI',Inter,sans-serif",
3497
3625
  "--sl-radius": `${host?.radius ?? 14}px`
3498
3626
  };
@@ -3624,7 +3752,13 @@ var SeatPicker = class _SeatPicker {
3624
3752
  // F3 minimap
3625
3753
  this.miniCanvas = null;
3626
3754
  this.miniBase = null;
3755
+ this.miniWrap = null;
3756
+ this.miniToggle = null;
3757
+ this.miniLabel = null;
3627
3758
  this.miniTf = null;
3759
+ this.miniDrag = null;
3760
+ this.miniSuppressClick = false;
3761
+ this.pickerLayoutSize = { width: 0, height: 0 };
3628
3762
  // F4 price-band filter — active band's category keys (null = all prices)
3629
3763
  this.priceBandKeys = null;
3630
3764
  this.focusedCatKey = null;
@@ -3725,6 +3859,7 @@ var SeatPicker = class _SeatPicker {
3725
3859
  mapTheme: options.theme?.map ?? null,
3726
3860
  onSelectionChange: () => {
3727
3861
  this.syncTray();
3862
+ this.refreshMinimap();
3728
3863
  if (this.committedSelection().length) this.collapseSectionCard();
3729
3864
  this.syncSelectionTo3d();
3730
3865
  },
@@ -3784,6 +3919,12 @@ var SeatPicker = class _SeatPicker {
3784
3919
  },
3785
3920
  // Tapped-section glide-in → surface (or clear) the section-summary card.
3786
3921
  onSectionFocus: (summary) => this.showSectionCard(summary),
3922
+ onDeckTap: () => {
3923
+ this.showSectionCard(null);
3924
+ this.syncFloors();
3925
+ this.syncRung();
3926
+ this.refreshMinimap();
3927
+ },
3787
3928
  onFocusSeat: (seat) => this.announceSeat(seat),
3788
3929
  onSeatHover: (d) => this.updateTooltip(d),
3789
3930
  onHint: (m) => {
@@ -4001,7 +4142,7 @@ var SeatPicker = class _SeatPicker {
4001
4142
  window.removeEventListener("keydown", this.fsEscHandler);
4002
4143
  this.fsEscHandler = null;
4003
4144
  }
4004
- requestAnimationFrame(() => this.controller.zoomToFit());
4145
+ requestAnimationFrame(() => this.controller.refitCurrentView());
4005
4146
  }
4006
4147
  setFsFallback(on) {
4007
4148
  if (this.fsFallback === on) return;
@@ -4017,7 +4158,7 @@ var SeatPicker = class _SeatPicker {
4017
4158
  window.removeEventListener("keydown", this.fsEscHandler);
4018
4159
  this.fsEscHandler = null;
4019
4160
  }
4020
- requestAnimationFrame(() => this.controller.zoomToFit());
4161
+ requestAnimationFrame(() => this.controller.refitCurrentView());
4021
4162
  }
4022
4163
  /**
4023
4164
  * Close the picker. In modal mode (SeatPicker.open()) this dismisses the
@@ -4157,11 +4298,17 @@ var SeatPicker = class _SeatPicker {
4157
4298
  }
4158
4299
  async render() {
4159
4300
  if (this.rendered) return this;
4301
+ const renderStartedAt = performance.now();
4302
+ const renderProfile = {};
4303
+ const duration = (startedAt) => Math.round((performance.now() - startedAt) * 100) / 100;
4160
4304
  this.rendered = true;
4161
4305
  ensureStyle();
4306
+ const localeStartedAt = performance.now();
4162
4307
  await loadLocale2(this.opts.locale);
4308
+ renderProfile.loadLocale = duration(localeStartedAt);
4163
4309
  if (this.opts.messages) setStringOverrides2(this.opts.messages);
4164
4310
  if (this.checkoutMode === "hosted") this.paymentOptions = this.pubApi.paymentOptions(this.opts.event);
4311
+ const shellStartedAt = performance.now();
4165
4312
  const mount = resolveContainer3(this.opts.container);
4166
4313
  const root = document.createElement("div");
4167
4314
  root.className = "sl-picker";
@@ -4185,7 +4332,7 @@ var SeatPicker = class _SeatPicker {
4185
4332
  this.syncTray();
4186
4333
  }
4187
4334
  });
4188
- Object.entries(resolveTokens(void 0, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
4335
+ Object.entries(resolvePickerTokens(void 0, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
4189
4336
  root.innerHTML = `
4190
4337
  <div class="sl-head">
4191
4338
  <div class="sl-logo" data-ref="logo"></div>
@@ -4264,14 +4411,20 @@ var SeatPicker = class _SeatPicker {
4264
4411
  const applyLayout = () => {
4265
4412
  const w = root.clientWidth;
4266
4413
  if (w <= 0) return;
4414
+ const h = root.clientHeight;
4415
+ const sizeChanged = w !== this.pickerLayoutSize.width || h !== this.pickerLayoutSize.height;
4416
+ this.pickerLayoutSize = { width: w, height: h };
4267
4417
  this.reportFramedHeight();
4268
4418
  const next = w < 640 ? "narrow" : "wide";
4269
- const density = root.clientHeight < 560 ? "compact" : "comfortable";
4270
- if (root.dataset.layout === next && root.dataset.density === density) return;
4271
- root.dataset.layout = next;
4272
- root.dataset.density = density;
4273
- if (next === "narrow" && !root.dataset.sheet) root.dataset.sheet = "peek";
4274
- this.dockLayoutChrome();
4419
+ const density = h < 560 ? "compact" : "comfortable";
4420
+ const layoutChanged = root.dataset.layout !== next || root.dataset.density !== density;
4421
+ if (layoutChanged) {
4422
+ root.dataset.layout = next;
4423
+ root.dataset.density = density;
4424
+ if (next === "narrow" && !root.dataset.sheet) root.dataset.sheet = "peek";
4425
+ this.dockLayoutChrome();
4426
+ }
4427
+ if (sizeChanged && this.miniBase) this.refreshMinimap();
4275
4428
  };
4276
4429
  this.ro = new ResizeObserver(applyLayout);
4277
4430
  this.ro.observe(root);
@@ -4284,7 +4437,7 @@ var SeatPicker = class _SeatPicker {
4284
4437
  this.fsChangeHandler = () => {
4285
4438
  if (!document.fullscreenElement) this.setFsFallback(false);
4286
4439
  this.syncFullscreenButtons();
4287
- requestAnimationFrame(() => this.controller.zoomToFit());
4440
+ requestAnimationFrame(() => this.controller.refitCurrentView());
4288
4441
  };
4289
4442
  document.addEventListener("fullscreenchange", this.fsChangeHandler);
4290
4443
  const head = this.els.sheetHead;
@@ -4386,7 +4539,10 @@ var SeatPicker = class _SeatPicker {
4386
4539
  const canvasHost = document.createElement("div");
4387
4540
  canvasHost.style.cssText = "position:absolute;inset:0";
4388
4541
  this.mapHost.appendChild(canvasHost);
4542
+ renderProfile.mountBuyerShell = duration(shellStartedAt);
4543
+ const chartStartedAt = performance.now();
4389
4544
  const info = await this.controller.render(canvasHost);
4545
+ renderProfile.loadChartAndStatuses = duration(chartStartedAt);
4390
4546
  if (this.destroyed) return this;
4391
4547
  if (!info) {
4392
4548
  this.els.boot.innerHTML = `<div class="sl-boot-title">${this.tf("picker.mapDidNotLoad", "The seat map didn\u2019t load")}</div><div>${this.tf("picker.checkConnection", "Check your connection and try again.")}</div><button type="button" class="sl-boot-retry">${this.tf("picker.accessRetry", "Try again")}</button>`;
@@ -4398,7 +4554,9 @@ var SeatPicker = class _SeatPicker {
4398
4554
  });
4399
4555
  return this;
4400
4556
  }
4557
+ Object.assign(renderProfile, info.performanceProfile);
4401
4558
  this.els.boot.remove();
4559
+ const controlsStartedAt = performance.now();
4402
4560
  this.startRealtime();
4403
4561
  this.salesClosed = !!info.salesClosed || !!this.opts.readOnly;
4404
4562
  root.dataset.eventMode = info.mode === "test" ? "test" : "live";
@@ -4414,7 +4572,7 @@ var SeatPicker = class _SeatPicker {
4414
4572
  this.regions["top-left"].appendChild(badge);
4415
4573
  }
4416
4574
  const chartTheme = this.controller.doc?.theme;
4417
- Object.entries(resolveTokens(chartTheme, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
4575
+ Object.entries(resolvePickerTokens(chartTheme, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
4418
4576
  this.currency = info.currency ?? this.opts.currency ?? "USD";
4419
4577
  this.eventTimezone = info.timezone ?? null;
4420
4578
  const logoUrl = this.opts.theme?.logoUrl ?? chartTheme?.logoUrl ?? info.posterUrl ?? null;
@@ -4522,12 +4680,34 @@ var SeatPicker = class _SeatPicker {
4522
4680
  this.buildPanelToggle();
4523
4681
  if (this.opts.panelCollapsed) this.setPanelCollapsed(true);
4524
4682
  this.dockLayoutChrome();
4683
+ renderProfile.buildBuyerControls = duration(controlsStartedAt);
4684
+ const holdStartedAt = performance.now();
4525
4685
  await this.restoreRememberedHold();
4686
+ renderProfile.restoreHold = duration(holdStartedAt);
4526
4687
  if (this.destroyed) return this;
4527
4688
  if (this.salesClosed) this.applySalesClosed();
4528
4689
  this.syncPrices();
4529
4690
  this.syncTray();
4530
4691
  this.resumeHostedOrder();
4692
+ const doc = this.controller.doc;
4693
+ this.emitAnalytics("chart_rendered", {
4694
+ renderingTimeMillis: duration(renderStartedAt),
4695
+ performanceProfile: renderProfile,
4696
+ seatCount: this.allSeats().length,
4697
+ objectCount: doc?.objects.length ?? 0,
4698
+ sectionCount: doc?.objects.filter((object) => object.type === "section").length ?? 0,
4699
+ view: this.buyerView,
4700
+ // Dimensions a host needs to read the timings above as anything other
4701
+ // than one undifferentiated number: WHICH chart, HOW big, WHICH data
4702
+ // path, and whether the browser had already warmed for this chart.
4703
+ // The event key is the chart's identity here — an event owns exactly one
4704
+ // chart — and the name is the same string already drawn in the header.
4705
+ eventKey: this.opts.event,
4706
+ chartName: doc?.name,
4707
+ floorCount: this.controller.getFloors().length,
4708
+ transport: this.opts.transport ? "custom" : "pubapi",
4709
+ load: markChartLoaded(this.opts.event) ? "repeat" : "cold"
4710
+ });
4531
4711
  return this;
4532
4712
  }
4533
4713
  /**
@@ -4589,7 +4769,8 @@ var SeatPicker = class _SeatPicker {
4589
4769
  const collapsed = this.sideCollapsed;
4590
4770
  btn.setAttribute("aria-expanded", String(!collapsed));
4591
4771
  btn.setAttribute("aria-label", collapsed ? this.tf("picker.showTicketPanel", "Show the ticket panel") : this.tf("picker.hideTicketPanel", "Hide the ticket panel"));
4592
- btn.innerHTML = collapsed ? `<svg viewBox="0 0 24 24"><path d="M15 6l-6 6 6 6"/></svg><span>${this.tf("picker.tickets", "Tickets")}</span>` : `<svg viewBox="0 0 24 24"><path d="M9 6l6 6-6 6"/></svg><span>${this.tf("picker.hidePanel", "Hide panel")}</span>`;
4772
+ btn.title = collapsed ? this.tf("picker.showTicketPanel", "Show the ticket panel") : this.tf("picker.hideTicketPanel", "Hide the ticket panel");
4773
+ btn.innerHTML = collapsed ? `<svg viewBox="0 0 24 24"><path d="M15 6l-6 6 6 6"/></svg><span>${this.tf("picker.tickets", "Tickets")}</span>` : `<svg viewBox="0 0 24 24"><path d="M9 6l6 6-6 6"/></svg>`;
4593
4774
  }
4594
4775
  /**
4595
4776
  * Collapse or reopen the wide-layout ticket panel in place (host-drivable —
@@ -4602,7 +4783,7 @@ var SeatPicker = class _SeatPicker {
4602
4783
  if (this.destroyed) return;
4603
4784
  this.sideCollapsed = collapsed;
4604
4785
  this.applyPanelCollapsed();
4605
- this.scheduleMotion(() => this.controller.zoomToFit(), 340);
4786
+ this.scheduleMotion(() => this.controller.refitCurrentView(), 340);
4606
4787
  }
4607
4788
  /** Idempotent DOM apply, also run on every layout flip (inert must lift on narrow). */
4608
4789
  applyPanelCollapsed() {
@@ -4997,10 +5178,77 @@ var SeatPicker = class _SeatPicker {
4997
5178
  buildMinimap() {
4998
5179
  const vp = this.controller.getViewport();
4999
5180
  if (!vp || !this.els.map) return;
5000
- const b = vp.bounds;
5001
- if (!(b.width > 0 && b.height > 0)) return;
5002
- const MAXW = 158;
5003
- const MAXH = 118;
5181
+ const host = this.regions["bottom-left"] ?? this.els.map;
5182
+ const toggle = document.createElement("button");
5183
+ toggle.type = "button";
5184
+ toggle.className = "sl-minimap-toggle";
5185
+ toggle.setAttribute("aria-label", this.tf("picker.openVenueMap", "Open venue overview map"));
5186
+ toggle.setAttribute("aria-expanded", "false");
5187
+ toggle.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 6l5-2 8 3 5-2v13l-5 2-8-3-5 2zM8 4v13M16 7v13"/></svg>';
5188
+ host.appendChild(toggle);
5189
+ this.miniToggle = toggle;
5190
+ const wrap = document.createElement("div");
5191
+ wrap.className = "sl-minimap";
5192
+ wrap.setAttribute("role", "group");
5193
+ const bar = document.createElement("div");
5194
+ bar.className = "sl-minimap-bar";
5195
+ const label = document.createElement("span");
5196
+ label.className = "sl-minimap-label";
5197
+ const key = document.createElement("span");
5198
+ key.className = "sl-minimap-key";
5199
+ key.setAttribute("aria-label", this.tf("picker.minimapStateKey", "Focused, selected, available, and inactive sections"));
5200
+ key.innerHTML = '<i class="focused" title="Focused"></i><i class="selected" title="Selected"></i><i title="Available"></i><i class="inactive" title="Inactive"></i>';
5201
+ const close = document.createElement("button");
5202
+ close.type = "button";
5203
+ close.className = "sl-minimap-close";
5204
+ close.setAttribute("aria-label", this.tf("picker.closeVenueMap", "Close venue overview map"));
5205
+ close.textContent = "\xD7";
5206
+ bar.append(label, key, close);
5207
+ const canvas = document.createElement("canvas");
5208
+ canvas.tabIndex = 0;
5209
+ canvas.setAttribute("role", "application");
5210
+ canvas.setAttribute("aria-label", this.tf("picker.venueMapInstructions", "Venue overview map. Drag the viewport, click a section, or use arrow keys to pan."));
5211
+ wrap.append(bar, canvas);
5212
+ host.appendChild(wrap);
5213
+ this.miniWrap = wrap;
5214
+ this.miniLabel = label;
5215
+ this.miniCanvas = canvas;
5216
+ const base = document.createElement("canvas");
5217
+ this.miniBase = base;
5218
+ toggle.addEventListener("click", () => this.setMinimapOpen(true));
5219
+ close.addEventListener("click", () => this.setMinimapOpen(false));
5220
+ canvas.addEventListener("click", (event) => {
5221
+ if (this.miniSuppressClick) {
5222
+ this.miniSuppressClick = false;
5223
+ return;
5224
+ }
5225
+ this.minimapJump(event);
5226
+ });
5227
+ canvas.addEventListener("pointerdown", (event) => this.minimapPointerDown(event));
5228
+ canvas.addEventListener("pointermove", (event) => this.minimapPointerMove(event));
5229
+ canvas.addEventListener("pointerup", (event) => this.minimapPointerUp(event));
5230
+ canvas.addEventListener("pointercancel", (event) => this.minimapPointerUp(event));
5231
+ canvas.addEventListener("keydown", (event) => this.minimapKeydown(event));
5232
+ this.layoutMinimap();
5233
+ this.syncMinimapLabel();
5234
+ this.drawMinimapStatic();
5235
+ this.drawMinimapRect();
5236
+ }
5237
+ setMinimapOpen(open) {
5238
+ this.root?.setAttribute("data-minimap-open", String(open));
5239
+ this.miniToggle?.setAttribute("aria-expanded", String(open));
5240
+ if (open) this.miniCanvas?.focus();
5241
+ else this.miniToggle?.focus();
5242
+ }
5243
+ /** Re-fit the thumbnail whenever the active floor or complete overview
5244
+ * changes bounds. This is presentation-only; the source chart is untouched. */
5245
+ layoutMinimap() {
5246
+ const canvas = this.miniCanvas;
5247
+ const base = this.miniBase;
5248
+ const b = this.controller.getViewport()?.bounds;
5249
+ if (!canvas || !base || !b || !(b.width > 0 && b.height > 0)) return;
5250
+ const MAXW = 196;
5251
+ const MAXH = 146;
5004
5252
  const PAD = 6;
5005
5253
  const aspect = b.width / Math.max(1, b.height);
5006
5254
  let w = MAXW;
@@ -5012,32 +5260,40 @@ var SeatPicker = class _SeatPicker {
5012
5260
  w = Math.max(64, w);
5013
5261
  h = Math.max(48, h);
5014
5262
  const dpr = Math.min(2, window.devicePixelRatio || 1);
5015
- const wrap = document.createElement("div");
5016
- wrap.className = "sl-minimap";
5017
- wrap.setAttribute("aria-hidden", "true");
5018
- const canvas = document.createElement("canvas");
5019
5263
  canvas.width = Math.round(w * dpr);
5020
5264
  canvas.height = Math.round(h * dpr);
5021
5265
  canvas.style.width = `${w}px`;
5022
5266
  canvas.style.height = `${h}px`;
5023
- wrap.appendChild(canvas);
5024
- (this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
5025
- this.miniCanvas = canvas;
5026
- const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
5027
- const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
5028
- const offY = (h * dpr - b.height * scale) / 2 - b.y * scale;
5029
- this.miniTf = { scale, offX, offY, dpr };
5030
- const base = document.createElement("canvas");
5031
5267
  base.width = canvas.width;
5032
5268
  base.height = canvas.height;
5033
- this.miniBase = base;
5034
- wrap.addEventListener("click", (e) => this.minimapJump(e));
5035
- this.drawMinimapStatic();
5036
- this.drawMinimapRect();
5269
+ this.miniTf = createMinimapTransform(b, canvas.width, canvas.height, dpr, PAD);
5270
+ }
5271
+ syncMinimapLabel() {
5272
+ if (!this.miniLabel || !this.miniWrap) return;
5273
+ const floors = this.controller.getFloors();
5274
+ const floor = this.controller.isFloorOverview() ? this.tf("picker.allFloors", "All floors") : floors.find((candidate) => candidate.id === this.controller.getActiveFloorId())?.name ?? this.tf("picker.venueOverview", "Venue overview");
5275
+ const sections = this.controller.getMinimapSnapshot().sections;
5276
+ const focused = sections.find((section) => section.active);
5277
+ const focusedLabel = focused?.label ?? this.lastSection?.label;
5278
+ const label = focusedLabel ? `${floor} \xB7 ${focusedLabel}` : floor;
5279
+ this.miniLabel.textContent = label;
5280
+ const selected = sections.filter((section) => section.state === "selected").length;
5281
+ const available = sections.filter((section) => section.state === "available").length;
5282
+ const inactive = sections.filter((section) => section.state === "inactive").length;
5283
+ this.miniWrap.setAttribute(
5284
+ "aria-label",
5285
+ sections.length ? `${label}. ${selected} selected, ${available} available, ${inactive} inactive sections.` : label
5286
+ );
5287
+ this.miniCanvas?.setAttribute(
5288
+ "aria-label",
5289
+ sections.length ? `${label}. ${selected} selected, ${available} available, ${inactive} inactive sections. ${this.tf("picker.venueMapInstructions", "The outlined region is the visible map area. Drag it, click a section, or use arrow keys to pan.")}` : `${label}. ${this.tf("picker.venueMapInstructions", "The outlined region is the visible map area. Drag it, click elsewhere, or use arrow keys to pan.")}`
5290
+ );
5037
5291
  }
5038
5292
  /** Repaint the static overview + rect (floor switch, live open/close). */
5039
5293
  refreshMinimap() {
5040
5294
  if (!this.miniBase) return;
5295
+ this.layoutMinimap();
5296
+ this.syncMinimapLabel();
5041
5297
  this.drawMinimapStatic();
5042
5298
  this.drawMinimapRect();
5043
5299
  }
@@ -5045,8 +5301,7 @@ var SeatPicker = class _SeatPicker {
5045
5301
  drawMinimapStatic() {
5046
5302
  const base = this.miniBase;
5047
5303
  const tf = this.miniTf;
5048
- const doc = this.controller.doc;
5049
- if (!base || !tf || !doc) return;
5304
+ if (!base || !tf) return;
5050
5305
  const ctx = base.getContext("2d");
5051
5306
  if (!ctx) return;
5052
5307
  ctx.clearRect(0, 0, base.width, base.height);
@@ -5055,34 +5310,42 @@ var SeatPicker = class _SeatPicker {
5055
5310
  const line = this.cssVar("--sl-line") || "rgba(139,147,167,.5)";
5056
5311
  const muted = this.cssVar("--sl-muted") || "#8b93a7";
5057
5312
  const accent = this.cssVar("--sl-accent") || "#6e7bff";
5058
- const zoneColor = new Map((doc.zones ?? []).map((z) => [z.id, z.color]));
5059
- let drewSection = false;
5060
- for (const o of this.activeFloorObjects()) {
5061
- if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
5062
- drewSection = true;
5063
- const closed = this.controller.isSectionClosed(o.id);
5064
- const fill = closed ? muted : o.color ?? (o.zone && zoneColor.get(o.zone)) ?? accent;
5313
+ const available = this.cssVar("--sl-success") || "#22a06b";
5314
+ const snapshot = this.controller.getMinimapSnapshot();
5315
+ for (const section of snapshot.sections) {
5316
+ if (section.outline.length < 3) continue;
5065
5317
  ctx.beginPath();
5066
- o.outline.forEach((p, i) => i === 0 ? ctx.moveTo(fx(p.x), fy(p.y)) : ctx.lineTo(fx(p.x), fy(p.y)));
5318
+ section.outline.forEach((point, index) => index === 0 ? ctx.moveTo(fx(point.x), fy(point.y)) : ctx.lineTo(fx(point.x), fy(point.y)));
5067
5319
  ctx.closePath();
5068
- ctx.globalAlpha = closed ? 0.26 : 0.42;
5069
- ctx.fillStyle = fill;
5320
+ ctx.globalAlpha = section.state === "inactive" ? 0.18 : section.state === "selected" ? 0.72 : 0.34;
5321
+ ctx.fillStyle = section.state === "inactive" ? muted : section.state === "selected" ? accent : available;
5070
5322
  ctx.fill();
5071
- ctx.globalAlpha = 0.85;
5323
+ ctx.globalAlpha = 0.82;
5072
5324
  ctx.lineWidth = Math.max(1, tf.dpr);
5073
5325
  ctx.strokeStyle = line;
5074
5326
  ctx.stroke();
5327
+ if (section.active) {
5328
+ ctx.globalAlpha = 1;
5329
+ ctx.lineJoin = "round";
5330
+ ctx.lineWidth = Math.max(4, tf.dpr * 3.5);
5331
+ ctx.strokeStyle = this.cssVar("--sl-surface") || "#fff";
5332
+ ctx.stroke();
5333
+ ctx.lineWidth = Math.max(2, tf.dpr * 1.75);
5334
+ ctx.strokeStyle = accent;
5335
+ ctx.stroke();
5336
+ }
5075
5337
  }
5076
5338
  ctx.globalAlpha = 1;
5077
- if (!drewSection) {
5339
+ if (!snapshot.sections.length) {
5078
5340
  const r = Math.max(1, tf.dpr);
5079
- for (const seat of expandChart(doc)) {
5080
- const cat = doc.categories.find((c) => c.key === seat.categoryKey);
5081
- ctx.fillStyle = cat?.color ?? accent;
5341
+ for (const seat of snapshot.seats) {
5342
+ ctx.globalAlpha = seat.state === "inactive" ? 0.22 : 0.78;
5343
+ ctx.fillStyle = seat.state === "inactive" ? muted : seat.state === "selected" ? accent : available;
5082
5344
  ctx.beginPath();
5083
5345
  ctx.arc(fx(seat.x), fy(seat.y), r, 0, Math.PI * 2);
5084
5346
  ctx.fill();
5085
5347
  }
5348
+ ctx.globalAlpha = 1;
5086
5349
  }
5087
5350
  }
5088
5351
  /** Blit the base overview, then stroke the current viewport rectangle on top. */
@@ -5097,41 +5360,106 @@ var SeatPicker = class _SeatPicker {
5097
5360
  ctx.drawImage(base, 0, 0);
5098
5361
  const vp = this.controller.getViewport();
5099
5362
  if (!vp) return;
5100
- const v = vp.visible;
5101
- const x = v.x * tf.scale + tf.offX;
5102
- const y = v.y * tf.scale + tf.offY;
5103
- const w = v.width * tf.scale;
5104
- const h = v.height * tf.scale;
5105
- const accent = this.cssVar("--sl-accent") || "#f4b740";
5363
+ const plan = minimapViewportPlan(vp.visible, tf, canvas.width, canvas.height, Math.max(2, tf.dpr * 2));
5364
+ const ink = this.cssVar("--sl-text") || "#12151c";
5365
+ const surface = this.cssVar("--sl-surface") || "#fff";
5106
5366
  ctx.save();
5107
- ctx.globalAlpha = 0.14;
5108
- ctx.fillStyle = accent;
5109
- ctx.fillRect(x, y, w, h);
5367
+ ctx.globalAlpha = 0.1;
5368
+ ctx.fillStyle = ink;
5369
+ ctx.fillRect(plan.raw.x, plan.raw.y, plan.raw.width, plan.raw.height);
5370
+ ctx.globalAlpha = 0.9;
5371
+ ctx.lineJoin = "round";
5372
+ ctx.lineWidth = Math.max(4, tf.dpr * 3.25);
5373
+ ctx.strokeStyle = surface;
5374
+ ctx.strokeRect(plan.raw.x, plan.raw.y, plan.raw.width, plan.raw.height);
5110
5375
  ctx.globalAlpha = 1;
5111
- ctx.lineWidth = Math.max(1.5, tf.dpr * 1.5);
5112
- ctx.strokeStyle = accent;
5113
- ctx.strokeRect(x, y, w, h);
5376
+ ctx.lineWidth = Math.max(2, tf.dpr * 1.75);
5377
+ ctx.strokeStyle = ink;
5378
+ ctx.strokeRect(plan.raw.x, plan.raw.y, plan.raw.width, plan.raw.height);
5379
+ if (plan.clipped && Object.values(plan.clippedEdges).some(Boolean)) {
5380
+ ctx.setLineDash([Math.max(3, tf.dpr * 3), Math.max(2, tf.dpr * 2)]);
5381
+ ctx.strokeRect(plan.clipped.x, plan.clipped.y, plan.clipped.width, plan.clipped.height);
5382
+ }
5114
5383
  ctx.restore();
5115
5384
  }
5116
- /** Minimap click → focus the section under the point (or overview on a miss). */
5117
- minimapJump(e) {
5385
+ minimapWorldPoint(event) {
5118
5386
  const canvas = this.miniCanvas;
5119
5387
  const tf = this.miniTf;
5120
- if (!canvas || !tf) return;
5121
- const r = canvas.getBoundingClientRect();
5122
- const px = (e.clientX - r.left) * (canvas.width / r.width);
5123
- const py = (e.clientY - r.top) * (canvas.height / r.height);
5124
- const wx = (px - tf.offX) / tf.scale;
5125
- const wy = (py - tf.offY) / tf.scale;
5126
- for (const o of this.activeFloorObjects()) {
5127
- if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
5128
- if (this.controller.isSectionClosed(o.id)) continue;
5129
- if (pointInPolygon(wx, wy, o.outline)) {
5130
- this.controller.focusSection(o.id);
5388
+ if (!canvas || !tf) return null;
5389
+ const rect = canvas.getBoundingClientRect();
5390
+ const px = (event.clientX - rect.left) * (canvas.width / Math.max(1, rect.width));
5391
+ const py = (event.clientY - rect.top) * (canvas.height / Math.max(1, rect.height));
5392
+ return { ...minimapPointToWorld({ x: px, y: py }, tf), px, py };
5393
+ }
5394
+ minimapPointerDown(event) {
5395
+ const point = this.minimapWorldPoint(event);
5396
+ const visible = this.controller.getViewport()?.visible;
5397
+ if (!point || !visible) return;
5398
+ const inside = point.x >= visible.x && point.x <= visible.x + visible.width && point.y >= visible.y && point.y <= visible.y + visible.height;
5399
+ if (!inside) return;
5400
+ this.miniDrag = {
5401
+ pointerId: event.pointerId,
5402
+ offsetX: point.x - (visible.x + visible.width / 2),
5403
+ offsetY: point.y - (visible.y + visible.height / 2),
5404
+ startX: event.clientX,
5405
+ startY: event.clientY,
5406
+ moved: false
5407
+ };
5408
+ this.miniCanvas?.setPointerCapture(event.pointerId);
5409
+ this.miniCanvas?.classList.add("sl-dragging");
5410
+ event.preventDefault();
5411
+ }
5412
+ minimapPointerMove(event) {
5413
+ const drag = this.miniDrag;
5414
+ if (!drag || drag.pointerId !== event.pointerId) return;
5415
+ const point = this.minimapWorldPoint(event);
5416
+ if (!point) return;
5417
+ if (!drag.moved && Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) > 2) {
5418
+ drag.moved = true;
5419
+ this.controller.clearSectionFocus();
5420
+ }
5421
+ this.controller.panToWorld({ x: point.x - drag.offsetX, y: point.y - drag.offsetY });
5422
+ event.preventDefault();
5423
+ }
5424
+ minimapPointerUp(event) {
5425
+ const drag = this.miniDrag;
5426
+ if (!drag || drag.pointerId !== event.pointerId) return;
5427
+ this.miniSuppressClick = drag.moved;
5428
+ this.miniDrag = null;
5429
+ this.miniCanvas?.classList.remove("sl-dragging");
5430
+ if (this.miniCanvas?.hasPointerCapture(event.pointerId)) this.miniCanvas.releasePointerCapture(event.pointerId);
5431
+ }
5432
+ minimapKeydown(event) {
5433
+ const visible = this.controller.getViewport()?.visible;
5434
+ if (!visible) return;
5435
+ if (event.key === "Home") {
5436
+ this.controller.overview();
5437
+ event.preventDefault();
5438
+ return;
5439
+ }
5440
+ const dx = event.key === "ArrowLeft" ? -visible.width * 0.2 : event.key === "ArrowRight" ? visible.width * 0.2 : 0;
5441
+ const dy = event.key === "ArrowUp" ? -visible.height * 0.2 : event.key === "ArrowDown" ? visible.height * 0.2 : 0;
5442
+ if (!dx && !dy) return;
5443
+ this.controller.clearSectionFocus();
5444
+ this.controller.panToWorld({
5445
+ x: visible.x + visible.width / 2 + dx,
5446
+ y: visible.y + visible.height / 2 + dy
5447
+ });
5448
+ event.preventDefault();
5449
+ }
5450
+ /** Minimap click → focus an available section, otherwise pan there. */
5451
+ minimapJump(e) {
5452
+ const point = this.minimapWorldPoint(e);
5453
+ if (!point) return;
5454
+ for (const section of this.controller.getMinimapSnapshot().sections) {
5455
+ if (section.state === "inactive" || section.outline.length < 3) continue;
5456
+ if (pointInPolygon(point.x, point.y, section.outline)) {
5457
+ this.controller.focusSection(section.id);
5131
5458
  return;
5132
5459
  }
5133
5460
  }
5134
- this.controller.overview();
5461
+ this.controller.clearSectionFocus();
5462
+ this.controller.panToWorld(point);
5135
5463
  }
5136
5464
  // ---- F4 price-band filter -------------------------------------------------
5137
5465
  /** Effective display price of a category: host pricing override → first tier → base. */
@@ -5140,32 +5468,39 @@ var SeatPicker = class _SeatPicker {
5140
5468
  if (chart === void 0 || !c.key) return chart;
5141
5469
  return this.paidPrice(c.key, c.tiers?.[0]?.id ?? null, chart);
5142
5470
  }
5143
- /** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
5471
+ /** Complete buyer-visible price range, including every ticket tier and
5472
+ * tier-specific host override. */
5473
+ catPriceRange(c) {
5474
+ const prices = c.tiers?.length ? c.tiers.map((tier) => this.paidPrice(c.key, tier.id ?? null, tier.price)) : c.price == null ? [] : [this.paidPrice(c.key, null, c.price)];
5475
+ return prices.length ? { min: Math.min(...prices), max: Math.max(...prices) } : void 0;
5476
+ }
5477
+ /** Derive category price bands without hiding the upper end of tier ranges. */
5144
5478
  priceBands() {
5145
5479
  const doc = this.controller.doc;
5146
5480
  if (!doc) return [];
5147
- const priced = doc.categories.map((c) => ({ key: c.key, price: this.catPrice(c) })).filter((x) => x.price != null);
5481
+ const priced = doc.categories.map((c) => ({ key: c.key, range: this.catPriceRange(c) })).filter((x) => x.range != null);
5148
5482
  if (!priced.length) return [];
5149
- const distinct = [...new Set(priced.map((p) => p.price))].sort((a, b) => a - b);
5483
+ const distinct = [...new Map(priced.map((item) => [`${item.range.min}:${item.range.max}`, item.range])).values()].sort((a, b) => a.min - b.min || a.max - b.max);
5150
5484
  if (distinct.length <= 5) {
5151
- return distinct.map((price) => ({
5152
- id: `p${price}`,
5153
- label: this.money(price),
5154
- keys: priced.filter((p) => p.price === price).map((p) => p.key),
5155
- min: price,
5156
- max: price
5485
+ return distinct.map((range) => ({
5486
+ id: `p${range.min}-${range.max}`,
5487
+ label: range.min === range.max ? this.money(range.min) : `${this.money(range.min)}\u2013${this.money(range.max)}`,
5488
+ keys: priced.filter((item) => item.range.min === range.min && item.range.max === range.max).map((item) => item.key),
5489
+ min: range.min,
5490
+ max: range.max
5157
5491
  }));
5158
5492
  }
5159
- const chunk = Math.ceil(distinct.length / 4);
5493
+ const ordered = [...priced].sort((a, b) => a.range.min - b.range.min || a.range.max - b.range.max);
5494
+ const chunk = Math.ceil(ordered.length / 4);
5160
5495
  const bands = [];
5161
- for (let i = 0; i < distinct.length; i += chunk) {
5162
- const slice = distinct.slice(i, i + chunk);
5163
- const lo = slice[0];
5164
- const hi = slice[slice.length - 1];
5496
+ for (let i = 0; i < ordered.length; i += chunk) {
5497
+ const slice = ordered.slice(i, i + chunk);
5498
+ const lo = Math.min(...slice.map((item) => item.range.min));
5499
+ const hi = Math.max(...slice.map((item) => item.range.max));
5165
5500
  bands.push({
5166
5501
  id: `b${i}`,
5167
5502
  label: lo === hi ? this.money(lo) : `${this.money(lo)}\u2013${this.money(hi)}`,
5168
- keys: priced.filter((p) => p.price >= lo && p.price <= hi).map((p) => p.key),
5503
+ keys: slice.map((item) => item.key),
5169
5504
  min: lo,
5170
5505
  max: hi
5171
5506
  });
@@ -5220,7 +5555,13 @@ var SeatPicker = class _SeatPicker {
5220
5555
  this.syncProjection();
5221
5556
  }
5222
5557
  if (hasSections) {
5223
- const RUNGS = ["zones", "sections", "seats"];
5558
+ const allObjects = [
5559
+ ...doc.objects,
5560
+ ...(doc.floors ?? []).flatMap((floor) => floor.objects)
5561
+ ];
5562
+ const authoredZoneIds = new Set((doc.zones ?? []).map((zone) => zone.id));
5563
+ const hasZones = allObjects.some((object) => object.type === "section" && typeof object.zone === "string" && authoredZoneIds.has(object.zone));
5564
+ const RUNGS = hasZones ? ["zones", "sections", "seats"] : ["sections", "seats"];
5224
5565
  const pills = document.createElement("div");
5225
5566
  pills.className = "sl-rungs on";
5226
5567
  pills.setAttribute("role", "group");
@@ -5253,18 +5594,38 @@ var SeatPicker = class _SeatPicker {
5253
5594
  const floors = this.controller.getFloors();
5254
5595
  const rail = document.createElement("div");
5255
5596
  rail.className = "sl-floors on";
5256
- rail.setAttribute("role", "group");
5257
- rail.setAttribute("aria-label", t2("picker.floor"));
5258
- rail.innerHTML = floors.map((f) => `<button type="button" data-floor="${f.id}">${f.name}</button>`).join("");
5259
- rail.querySelectorAll("button").forEach((btn) => {
5260
- btn.addEventListener("click", () => {
5261
- this.controller.setFloor(btn.dataset.floor);
5262
- this.showSectionCard(null);
5263
- this.syncFloors();
5264
- this.syncRung();
5265
- this.refreshMinimap();
5266
- });
5597
+ const select = document.createElement("select");
5598
+ select.setAttribute("aria-label", this.tf("picker.chooseLevel", "Choose level"));
5599
+ const overview = document.createElement("option");
5600
+ overview.value = "";
5601
+ overview.textContent = this.tf("picker.allFloors", "All floors");
5602
+ select.appendChild(overview);
5603
+ for (const floor of floors) {
5604
+ const option = document.createElement("option");
5605
+ option.value = floor.id;
5606
+ option.textContent = floor.name;
5607
+ select.appendChild(option);
5608
+ }
5609
+ select.addEventListener("change", () => {
5610
+ if (select.value === "") this.controller.setFloorOverview(true);
5611
+ else this.controller.setFloor(select.value);
5612
+ this.showSectionCard(null);
5613
+ this.syncFloors();
5614
+ this.syncRung();
5615
+ this.refreshMinimap();
5267
5616
  });
5617
+ rail.appendChild(select);
5618
+ const info = document.createElement("button");
5619
+ info.type = "button";
5620
+ info.className = "sl-floor-info";
5621
+ info.textContent = "i";
5622
+ const explanation = this.tf(
5623
+ "picker.floorHelp",
5624
+ "All floors shows the complete venue. Choose one level to inspect and select its seats."
5625
+ );
5626
+ info.setAttribute("aria-label", explanation);
5627
+ info.title = explanation;
5628
+ rail.appendChild(info);
5268
5629
  this.regions["left-rail"].appendChild(rail);
5269
5630
  this.floorsEl = rail;
5270
5631
  this.syncFloors();
@@ -5310,19 +5671,59 @@ var SeatPicker = class _SeatPicker {
5310
5671
  syncFloors() {
5311
5672
  if (!this.floorsEl) return;
5312
5673
  const active = this.controller.getActiveFloorId();
5313
- this.floorsEl.querySelectorAll("button").forEach((btn) => {
5314
- btn.classList.toggle("on", btn.dataset.floor === active);
5315
- });
5674
+ const overview = this.controller.isFloorOverview();
5675
+ const select = this.floorsEl.querySelector("select");
5676
+ if (select) select.value = overview ? "" : active;
5316
5677
  }
5317
5678
  /** Show (or clear, on null) the tapped-section summary card. */
5318
5679
  showSectionCard(summary) {
5319
5680
  this.lastSection = summary;
5320
5681
  this.secCardEl?.remove();
5321
5682
  this.secCardEl = null;
5322
- if (!summary) return;
5683
+ if (!summary) {
5684
+ this.refreshMinimap();
5685
+ return;
5686
+ }
5323
5687
  this.secCardCollapsed = this.controller.getRung() === "seats";
5324
5688
  this.secCardShownAt = Date.now();
5325
5689
  this.renderSectionCard(summary);
5690
+ this.refreshMinimap();
5691
+ }
5692
+ /**
5693
+ * Open sections next to the focused one, in authored order.
5694
+ *
5695
+ * This is intentionally a circular ring: arena sections are usually authored
5696
+ * clockwise, so moving past the last section should reach the first without
5697
+ * forcing a buyer back through the overview level. Closed or empty sections
5698
+ * are skipped because focusing one cannot produce a useful buyer card.
5699
+ */
5700
+ sectionNeighbours(sectionId) {
5701
+ const sections = this.activeFloorObjects().filter((object) => object.type === "section" && !this.controller.isSectionClosed(object.id) && this.controller.getSectionSeatCount(object.id) > 0);
5702
+ if (sections.length < 2) return null;
5703
+ const index = sections.findIndex((section) => section.id === sectionId);
5704
+ if (index < 0) return null;
5705
+ return {
5706
+ previous: sections[(index - 1 + sections.length) % sections.length],
5707
+ next: sections[(index + 1) % sections.length]
5708
+ };
5709
+ }
5710
+ /** Previous/next controls shared by expanded, mini, and phone section cards. */
5711
+ sectionNavigationHtml(summary) {
5712
+ const neighbours = this.sectionNeighbours(summary.id);
5713
+ if (!neighbours) return "";
5714
+ const previousLabel = neighbours.previous.label || neighbours.previous.id;
5715
+ const nextLabel = neighbours.next.label || neighbours.next.id;
5716
+ return `<span class="sl-seccard-nav" aria-label="${this.tf("picker.browseSections", "Browse sections")}"><button type="button" data-section-nav="${escapeOption(neighbours.previous.id)}" aria-label="${this.tf("picker.previousSection", "Previous section")}: ${escapeOption(previousLabel)}" title="${escapeOption(previousLabel)}">\u2190</button><button type="button" data-section-nav="${escapeOption(neighbours.next.id)}" aria-label="${this.tf("picker.nextSection", "Next section")}: ${escapeOption(nextLabel)}" title="${escapeOption(nextLabel)}">\u2192</button></span>`;
5717
+ }
5718
+ wireSectionNavigation(card) {
5719
+ card.querySelectorAll("[data-section-nav]").forEach((button) => {
5720
+ button.addEventListener("click", (event) => {
5721
+ event.preventDefault();
5722
+ event.stopPropagation();
5723
+ const sectionId = button.dataset.sectionNav;
5724
+ if (sectionId) this.controller.focusSection(sectionId);
5725
+ });
5726
+ });
5326
5727
  }
5327
5728
  /**
5328
5729
  * Render the section card in the form the layout + state want: expanded card
@@ -5336,26 +5737,32 @@ var SeatPicker = class _SeatPicker {
5336
5737
  this.lastSection = null;
5337
5738
  return;
5338
5739
  }
5339
- const paid = summary.categories.length ? summary.categories.map((c) => this.paidPrice(c.key, null, c.price)) : [summary.priceMin, summary.priceMax];
5340
- const paidMin = Math.min(...paid);
5341
- const paidMax = Math.max(...paid);
5740
+ const paidRanges = summary.categories.length ? summary.categories.map((item) => {
5741
+ const category = this.controller.doc?.categories.find((candidate) => candidate.key === item.key);
5742
+ return category ? this.catPriceRange(category) : { min: item.priceMin, max: item.priceMax };
5743
+ }).filter((range) => range != null) : [{ min: summary.priceMin, max: summary.priceMax }];
5744
+ const paidMin = Math.min(...paidRanges.map((range) => range.min));
5745
+ const paidMax = Math.max(...paidRanges.map((range) => range.max));
5342
5746
  const priceLabel = paidMin === paidMax ? this.money(paidMin) : `${this.money(paidMin)}\u2013${this.money(paidMax)}`;
5343
5747
  const leftLabel = tCount("picker.seatsLeftInSection", summary.seatsLeft);
5344
5748
  const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${t2("picker.closeSectionSummary")}">\u2715</button>`;
5749
+ const sectionNav = this.sectionNavigationHtml(summary);
5345
5750
  const card = document.createElement("div");
5346
5751
  const narrow = this.root?.dataset.layout === "narrow";
5347
5752
  if (narrow) {
5348
5753
  card.className = "sl-seccard strip on";
5349
5754
  card.setAttribute("role", "status");
5350
5755
  card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
5351
- card.innerHTML = `<span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span><span class="sl-seccard-left">${leftLabel}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn;
5756
+ card.innerHTML = `<span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span><span class="sl-seccard-left">${leftLabel}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + sectionNav + xBtn;
5757
+ this.wireSectionNavigation(card);
5352
5758
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
5353
5759
  (this.els.sheetHead ?? this.els.side ?? this.els.map).appendChild(card);
5354
5760
  } else if (this.secCardCollapsed) {
5355
5761
  card.className = "sl-seccard mini on";
5356
5762
  card.setAttribute("role", "button");
5357
5763
  card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
5358
- card.innerHTML = `<span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span><span class="sl-seccard-left">${leftLabel}</span>` + xBtn;
5764
+ card.innerHTML = `<span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span><span class="sl-seccard-left">${leftLabel}</span>` + sectionNav + xBtn;
5765
+ this.wireSectionNavigation(card);
5359
5766
  card.addEventListener("click", (e) => {
5360
5767
  if (e.target.closest(".sl-seccard-x")) return;
5361
5768
  this.secCardCollapsed = false;
@@ -5370,9 +5777,13 @@ var SeatPicker = class _SeatPicker {
5370
5777
  card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
5371
5778
  const mix = summary.categories.map((c) => {
5372
5779
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
5373
- return `<span class="sl-seccard-mix-item${dim ? " sl-dim" : ""}"><span class="sl-seccard-mix-dot" style="background:${c.color}"></span>${c.label} <span class="sl-seccard-mix-price">${this.money(this.paidPrice(c.key, null, c.price))}</span></span>`;
5780
+ const category = this.controller.doc?.categories.find((candidate) => candidate.key === c.key);
5781
+ const range = category ? this.catPriceRange(category) : { min: c.priceMin, max: c.priceMax };
5782
+ const rangeLabel = range && range.min !== range.max ? `${this.money(range.min)}\u2013${this.money(range.max)}` : this.money(range?.min ?? c.price);
5783
+ return `<span class="sl-seccard-mix-item${dim ? " sl-dim" : ""}"><span class="sl-seccard-mix-dot" style="background:${c.color}"></span>${c.label} <span class="sl-seccard-mix-price">${rangeLabel}</span></span>`;
5374
5784
  }).join("");
5375
- card.innerHTML = `<div class="sl-seccard-head"><span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn + `</div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${leftLabel}</span></div>` + (summary.entrance ? `<div class="sl-seccard-entrance">${t2("picker.entrance")} ${String(summary.entrance).replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch])}</div>` : "") + (mix ? `<div class="sl-seccard-mix">${mix}</div>` : "") + `<div class="sl-seccard-foot"><button type="button" class="sl-seccard-overview">\u2190 ${t2("picker.overview")}</button><span class="sl-seccard-hint">${t2("picker.tapSeatHint")}</span></div>`;
5785
+ card.innerHTML = `<div class="sl-seccard-head"><span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn + `</div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${leftLabel}</span></div>` + (summary.entrance ? `<div class="sl-seccard-entrance">${t2("picker.entrance")} ${String(summary.entrance).replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch])}</div>` : "") + (mix ? `<div class="sl-seccard-mix">${mix}</div>` : "") + `<div class="sl-seccard-foot"><button type="button" class="sl-seccard-overview">\u2190 ${t2("picker.overview")}</button>` + sectionNav + `<span class="sl-seccard-hint">${t2("picker.tapSeatHint")}</span></div>`;
5786
+ this.wireSectionNavigation(card);
5376
5787
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
5377
5788
  card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
5378
5789
  (this.regions["top-center"] ?? this.els.map).appendChild(card);
@@ -5437,13 +5848,14 @@ var SeatPicker = class _SeatPicker {
5437
5848
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
5438
5849
  const status = this.controller.getStatus(seat.id) ?? "free";
5439
5850
  const statusText = status === "free" ? this.tf("picker.statusAvailable", "available") : status === "held" ? this.tf("picker.statusOnHold", "on hold") : this.tf("picker.statusTaken2", "taken");
5440
- const price = cat ? this.catPrice(cat) : void 0;
5851
+ const priceRange = cat ? this.catPriceRange(cat) : void 0;
5852
+ const priceText = priceRange ? priceRange.min === priceRange.max ? this.money(priceRange.min) : `${this.money(priceRange.min)}\u2013${this.money(priceRange.max)}` : void 0;
5441
5853
  const table = this.controller.tableSelection(seat.id);
5442
5854
  const details = table ?? this.controller.seatDetails(seat.id);
5443
5855
  const typeWord = this.rowTypeWord(details);
5444
5856
  const buyerName = details?.rowLabel ?? details?.displayLabel ?? seat.displayLabel ?? seat.label;
5445
5857
  const identity = table ? `${typeWord} ${buyerName}, ${table.bookingMode === "variable" ? t2("picker.minToMaxGuests", { min: table.minOccupancy, max: table.maxOccupancy }) : t2("picker.capacityGuests", { count: table.capacity })}` : details?.objectType === "booth" ? `${typeWord} ${buyerName}` : details?.objectType === "table" ? `${typeWord} ${buyerName}, ${t2("picker.seatNumberLower", { label: details.seatNumber ?? seat.label })}` : t2("picker.seatLabel", { label: details?.displayLabel ?? seat.displayLabel ?? seat.label });
5446
- this.srEl.textContent = `${identity}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
5858
+ this.srEl.textContent = `${identity}, ${cat?.label ?? seat.categoryKey}${priceText ? `, ${priceText}` : ""}, ${statusText}`;
5447
5859
  }
5448
5860
  // ---- atomic table confirmation / guest quantity ---------------------------
5449
5861
  showTableDialog(table, held, returnFocus) {
@@ -5610,12 +6022,22 @@ var SeatPicker = class _SeatPicker {
5610
6022
  details?.rowLabel || details?.objectType === "booth" ? `<div class="sl-confirm-field"><span class="sl-confirm-key">${safe(this.rowTypeWord(details))}</span><span class="sl-confirm-value">${safe(this.rowShort(details) ?? details?.displayLabel ?? seat.displayLabel ?? seat.label)}</span></div>` : "",
5611
6023
  details?.objectType !== "booth" ? `<div class="sl-confirm-field"><span class="sl-confirm-key">${this.tf("picker.seat", "Seat")}</span><span class="sl-confirm-value">${safe(details?.seatNumber ?? details?.displayLabel ?? seat.displayLabel ?? seat.label)}</span></div>` : ""
5612
6024
  ].filter(Boolean).join("");
6025
+ const tiers = details?.tiers ?? cat?.tiers ?? [];
6026
+ const selectedTierId = details?.tierId ?? tiers[0]?.id;
6027
+ const tierGuidance = (tier) => tier.buyerMessage?.trim() || (tier.restriction === "companion" ? this.tf("picker.companionRequiresWheelchair", "Requires the adjacent wheelchair place") : void 0);
6028
+ const tierChoices = tiers.length > 1 ? `<fieldset class="sl-confirm-tiers"><legend>${this.tf("picker.ticketType", "Ticket type")}</legend>` + tiers.map((tier) => {
6029
+ const guidance = tierGuidance(tier);
6030
+ return `<button type="button" class="sl-confirm-tier" data-confirm-tier="${safe(tier.id)}" aria-pressed="${tier.id === selectedTierId}"><span class="sl-confirm-tier-copy"><span class="sl-confirm-tier-name">${safe(tier.name)}</span>` + (guidance ? `<small class="sl-confirm-tier-note">${safe(guidance)}</small>` : "") + `</span><span class="sl-confirm-tier-price">${this.money(this.paidPrice(seat.categoryKey, tier.id, tier.price))}</span></button>`;
6031
+ }).join("") + `</fieldset>` : tiers[0] && tierGuidance(tiers[0]) ? `<p class="sl-confirm-tier-note">${safe(tierGuidance(tiers[0]))}</p>` : "";
5613
6032
  const el = document.createElement("div");
5614
6033
  el.className = "sl-confirm";
5615
6034
  el.setAttribute("role", "dialog");
5616
6035
  el.setAttribute("aria-label", t2("picker.confirmSeatLabel", { label: seat.label }));
5617
- el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
5618
- el.innerHTML = `<div class="sl-confirm-grid">` + identityFields + `</div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + this.wheelchairConfirmHtml(details?.wheelchairSpaceType) + this.commercialConfirmHtml(seat.commercial) + (this.seatViewEnabled() && this.buyerView !== "venue3d" ? this.confirmThumbHtml(seat) : "") + (this.buyerView === "venue3d" ? `${this.seatConfidenceConfirmHtml(seat, `${details?.displayLabel ?? seat.displayLabel ?? seat.label} \xB7 ${price == null ? this.tf("picker.priceNotSupplied", "Price not supplied") : this.money(price)}`)}<div class="sl-confirm-inspect-row">${this.view3dCompareConfirmHtml(seat)}${this.see3dConfirmHtml()}</div>` : this.see3dConfirmHtml()) + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">${t2("common.cancel")}</button><button type="button" class="sl-confirm-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4 4L19 7"/></svg>${this.tf("picker.select", "Select")}</button></div></div>`;
6036
+ const categoryColor = cat?.color ?? "#6e7bff";
6037
+ const categoryBand = mixThemeColors(categoryColor, this.cssVar("--sl-surface") || "#1a2234", 0.76) ?? categoryColor;
6038
+ el.style.setProperty("--sl-cat", categoryColor);
6039
+ el.style.setProperty("--sl-cat-ink", highestContrastInk(categoryBand, "#172033", "#ffffff"));
6040
+ el.innerHTML = `<div class="sl-confirm-grid">` + identityFields + `</div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + tierChoices + this.wheelchairConfirmHtml(details?.wheelchairSpaceType) + this.commercialConfirmHtml(seat.commercial) + (this.seatViewEnabled() && this.buyerView !== "venue3d" ? this.confirmThumbHtml(seat) : "") + (this.buyerView === "venue3d" ? `${this.seatConfidenceConfirmHtml(seat, `${details?.displayLabel ?? seat.displayLabel ?? seat.label} \xB7 ${price == null ? this.tf("picker.priceNotSupplied", "Price not supplied") : this.money(price)}`)}<div class="sl-confirm-inspect-row">${this.view3dCompareConfirmHtml(seat)}${this.see3dConfirmHtml()}</div>` : this.see3dConfirmHtml()) + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">${t2("common.cancel")}</button><button type="button" class="sl-confirm-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4 4L19 7"/></svg>${this.tf("picker.select", "Select")}</button></div></div>`;
5619
6041
  this.els.map.appendChild(el);
5620
6042
  this.confirmEl = el;
5621
6043
  const thumb = el.querySelector(".sl-confirm-thumb");
@@ -5631,6 +6053,22 @@ var SeatPicker = class _SeatPicker {
5631
6053
  this.openSeatConfidencePassport(seat, event.currentTarget instanceof HTMLElement ? event.currentTarget : null);
5632
6054
  });
5633
6055
  el.querySelector(".sl-confirm-compare")?.addEventListener("click", () => this.saveView3dComparisonSeat(seat));
6056
+ el.querySelectorAll("[data-confirm-tier]").forEach((button) => {
6057
+ button.addEventListener("click", () => {
6058
+ const tierId = button.dataset.confirmTier;
6059
+ const tier = tiers.find((candidate) => candidate.id === tierId);
6060
+ if (!tier) return;
6061
+ this.controller.setSeatTier(seat.id, tier.id);
6062
+ el.querySelectorAll("[data-confirm-tier]").forEach((candidate) => {
6063
+ candidate.setAttribute("aria-pressed", String(candidate === button));
6064
+ });
6065
+ const priceElement = el.querySelector(".sl-confirm-price");
6066
+ if (priceElement) {
6067
+ priceElement.textContent = this.money(this.paidPrice(seat.categoryKey, tier.id, tier.price));
6068
+ }
6069
+ this.reanchorConfirm();
6070
+ });
6071
+ });
5634
6072
  el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
5635
6073
  if (this.buyerView === "venue3d") {
5636
6074
  this.dismissConfirm();
@@ -6007,21 +6445,23 @@ var SeatPicker = class _SeatPicker {
6007
6445
  const doc = this.controller.doc;
6008
6446
  if (!doc || !this.els.prices) return;
6009
6447
  const left = this.controller.categoryAvailability();
6010
- this.narrateAvailability(doc.categories, left);
6011
- this.syncSoldout(doc.categories, left);
6448
+ const sellable = doc.categories.filter((c) => !c.notForSale);
6449
+ this.narrateAvailability(sellable, left);
6450
+ this.syncSoldout(sellable, left);
6012
6451
  const PRICE_LIMIT = 5;
6013
- const overflow = doc.categories.length - PRICE_LIMIT;
6452
+ const overflow = sellable.length - PRICE_LIMIT;
6014
6453
  const collapsed = overflow > 1 && !this.pricesExpanded;
6015
- const shown = collapsed ? doc.categories.slice(0, PRICE_LIMIT) : doc.categories;
6454
+ const shown = collapsed ? sellable.slice(0, PRICE_LIMIT) : sellable;
6016
6455
  this.els.prices.classList.toggle("sl-expanded", overflow > 1 && this.pricesExpanded);
6017
6456
  this.els.prices.innerHTML = shown.map((c) => {
6018
6457
  const price = this.catPrice(c);
6458
+ const range = this.catPriceRange(c);
6019
6459
  const offer = this.offerPrice(c.key);
6020
6460
  const previous = offer?.previousPrice != null && offer.previousPrice > offer.price ? offer.previousPrice : null;
6021
6461
  const active = this.focusedCatKey === c.key;
6022
6462
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
6023
- return `<div class="sl-price-row${dim ? " sl-dim" : ""}${active ? " sl-active" : ""}" data-cat="${escapeOption(c.key)}" role="button" tabindex="0" aria-pressed="${active}" title="${escapeOption(active ? this.tf("picker.showAllSeats", "Show all seats") : t2("picker.showLabelSeatsOnMap", { label: c.label }))}"><span class="sl-dot" style="background:${escapeOption(c.color)}"></span><span class="sl-price-label">${escapeOption(c.label)}` + (offer?.offerName ? `<small class="sl-price-offer">${escapeOption(offer.offerName)}</small>` : "") + `</span><span class="sl-price-left">${tCount("picker.leftCount", left[c.key] ?? 0)}</span>` + (previous != null ? `<span class="sl-price-was">${escapeOption(this.money(previous))}</span>` : "") + (price != null ? `<span class="sl-price-amt">${this.money(price)}</span>` : "") + `</div>`;
6024
- }).join("") + (overflow > 1 ? `<button type="button" class="sl-price-more" aria-expanded="${!collapsed}">` + (collapsed ? t2("picker.showAllTicketTypes", { count: doc.categories.length }) : this.tf("picker.showFewer", "Show fewer")) + `</button>` : "") + `<div class="sl-status-key" aria-label="${this.tf("picker.seatStatusLegend", "Seat status legend")}"><span class="sl-status-item"><i class="sl-status-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></i>${this.tf("picker.temporarilyHeld", "Temporarily held")}</span><span class="sl-status-item"><i class="sl-status-icon sold" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 17L17 7"/></svg></i>${this.tf("picker.sold", "Sold")}</span></div>`;
6463
+ return `<div class="sl-price-row${dim ? " sl-dim" : ""}${active ? " sl-active" : ""}" data-cat="${escapeOption(c.key)}" role="button" tabindex="0" aria-pressed="${active}" title="${escapeOption(active ? this.tf("picker.showAllSeats", "Show all seats") : t2("picker.showLabelSeatsOnMap", { label: c.label }))}"><span class="sl-dot" style="background:${escapeOption(c.color)}"></span><span class="sl-price-label">${escapeOption(c.label)}` + (offer?.offerName ? `<small class="sl-price-offer">${escapeOption(offer.offerName)}</small>` : "") + `</span><span class="sl-price-left">${tCount("picker.leftCount", left[c.key] ?? 0)}</span>` + (previous != null ? `<span class="sl-price-was">${escapeOption(this.money(previous))}</span>` : "") + (range != null ? `<span class="sl-price-amt">${range.min === range.max ? this.money(range.min) : `${this.money(range.min)}\u2013${this.money(range.max)}`}</span>` : price != null ? `<span class="sl-price-amt">${this.money(price)}</span>` : "") + `</div>`;
6464
+ }).join("") + (overflow > 1 ? `<button type="button" class="sl-price-more" aria-expanded="${!collapsed}">` + (collapsed ? t2("picker.showAllTicketTypes", { count: sellable.length }) : this.tf("picker.showFewer", "Show fewer")) + `</button>` : "") + `<div class="sl-status-key" aria-label="${this.tf("picker.seatStatusLegend", "Seat status legend")}"><span class="sl-status-item"><i class="sl-status-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></i>${this.tf("picker.temporarilyHeld", "Temporarily held")}</span><span class="sl-status-item"><i class="sl-status-icon sold" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 17L17 7"/></svg></i>${this.tf("picker.sold", "Sold")}</span></div>`;
6025
6465
  this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
6026
6466
  row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
6027
6467
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
@@ -6125,7 +6565,7 @@ var SeatPicker = class _SeatPicker {
6125
6565
  const noPicks = !seats.length && !heldItems.length && !this.pendingGACount();
6126
6566
  if (noPicks) this.baReopen = false;
6127
6567
  if (!this.salesClosed && !this.hold && (noPicks || this.baReopen || this.bestAvailableBusy || this.bestAvailableConfirm)) {
6128
- const cats = this.controller.doc?.categories ?? [];
6568
+ const cats = (this.controller.doc?.categories ?? []).filter((c) => !c.notForSale);
6129
6569
  const zones = this.controller.getBestAvailableZones();
6130
6570
  if (this.baZone && !zones.some((zone) => zone.id === this.baZone)) this.baZone = "";
6131
6571
  parts.push(this.bestAvailableConfirm ? `<div class="sl-ba" role="alert"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>${this.tf("picker.replaceCurrentChoices", "Replace your current choices?")}</div><div class="sl-ba-replace"><b>${t2("picker.willFindSeatsTogether", { count: this.baQty })}</b><span>${this.tf("picker.manualTicketsRemovedNote", "Your manually selected tickets will be removed only after a new group is secured.")}</span></div><div class="sl-ba-actions"><button type="button" data-ba-cancel>${this.tf("picker.keepMine", "Keep mine")}</button><button type="button" class="replace" data-ba-replace>${this.tf("picker.findNewSeats", "Find new seats")}</button></div></div>` : `<div class="sl-ba"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>${this.tf("picker.findBestSeatsTogether", "Find the best seats together")}</div><div class="sl-ba-copy"><span class="wide">${this.tf("picker.willChooseClosestGroup", "We\u2019ll choose the closest available group for you.")}</span><span class="narrow">${this.tf("picker.closestGroupChosenInstantly", "Closest available group, chosen instantly.")}</span></div>` + // Premium quick-pick — present only when the chart actually has premium
@@ -6181,8 +6621,10 @@ var SeatPicker = class _SeatPicker {
6181
6621
  nextTrayKeys.add(itemKey);
6182
6622
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
6183
6623
  const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(this.paidPrice(s.categoryKey, ti.id, ti.price))}</option>`).join("") + `</select>` : "";
6624
+ const chosenTier = s.tiers?.find((tier) => tier.id === s.tierId) ?? s.tiers?.[0];
6625
+ const tierNote = chosenTier?.buyerMessage?.trim() || (chosenTier?.restriction === "companion" ? this.tf("picker.companionRequiresWheelchair", "Requires the adjacent wheelchair place") : "");
6184
6626
  parts.push(
6185
- `<div class="sl-chip${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-seat="${s.id}" data-locate="${s.id}"><div class="sl-chip-main">` + idGrid(s.id, s.label, s.objectType, s.quantity ?? 1, s.objectId, s) + `<div class="sl-chip-sub"><span class="sl-ticket-state" aria-label="${this.tf("picker.selected", "Selected")}" title="${this.tf("picker.selected", "Selected")}"><svg viewBox="0 0 24 24"><path d="M5 12l4 4L19 6"/></svg></span><span class="cat">${cat?.label ?? s.categoryKey}</span>` + (s.objectType === "table" && s.bookingMode === "variable" ? `<button type="button" class="sl-table-edit" data-table-edit="${encodeURIComponent(s.label)}">${t2("picker.guestsCountEdit", { count: s.quantity ?? 1 })}</button>` : "") + `${this.wheelchairChipMarker(s.wheelchairSpaceType)}${this.commercialChipMarker(s.commercial)}${tierSelect}<span class="amt">${this.money(this.paidPrice(s.categoryKey, s.tierId ?? null, s.price) * (s.quantity ?? 1))}</span></div></div>` + iconRail(t2("picker.removeSeatLabel", { label: s.label }), canView && s.objectType !== "table" ? s.label : null) + `</div>`
6627
+ `<div class="sl-chip${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-seat="${s.id}" data-locate="${s.id}"><div class="sl-chip-main">` + idGrid(s.id, s.label, s.objectType, s.quantity ?? 1, s.objectId, s) + `<div class="sl-chip-sub"><span class="sl-ticket-state" aria-label="${this.tf("picker.selected", "Selected")}" title="${this.tf("picker.selected", "Selected")}"><svg viewBox="0 0 24 24"><path d="M5 12l4 4L19 6"/></svg></span><span class="cat">${cat?.label ?? s.categoryKey}</span>` + (s.objectType === "table" && s.bookingMode === "variable" ? `<button type="button" class="sl-table-edit" data-table-edit="${encodeURIComponent(s.label)}">${t2("picker.guestsCountEdit", { count: s.quantity ?? 1 })}</button>` : "") + `${this.wheelchairChipMarker(s.wheelchairSpaceType)}${this.commercialChipMarker(s.commercial)}${tierSelect}<span class="amt">${this.money(this.paidPrice(s.categoryKey, s.tierId ?? null, s.price) * (s.quantity ?? 1))}</span></div>` + (tierNote ? `<small class="sl-tier-note">${escapeOption(tierNote)}</small>` : "") + `</div>` + iconRail(t2("picker.removeSeatLabel", { label: s.label }), canView && s.objectType !== "table" ? s.label : null) + `</div>`
6186
6628
  );
6187
6629
  }
6188
6630
  for (const area of gaAreas) {
@@ -6495,7 +6937,7 @@ var SeatPicker = class _SeatPicker {
6495
6937
  const problem = err;
6496
6938
  const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
6497
6939
  if (problem.reason === "event_closed") this.setSalesClosed(true);
6498
- const message = problem.reason === "event_closed" ? this.tf("picker.seatSalesClosedForEvent", "Seat sales have closed for this event.") : labels.length ? labels.length === 1 ? t2("picker.labelsNoLongerAvailable.one", { labels: labels.join(", ") }) : t2("picker.labelsNoLongerAvailable.other", { labels: labels.join(", ") }) : this.tf("picker.seatsJustTaken", "One or more seats were just taken. Please pick again.");
6940
+ const message = problem.reason === "event_closed" ? this.tf("picker.seatSalesClosedForEvent", "Seat sales have closed for this event.") : problem.reason === "companion_pair_required" ? this.tf("picker.companionPairRequired", "Select the adjacent wheelchair place with each companion ticket.") : problem.reason === "ticket_option_not_applicable" ? this.tf("picker.ticketOptionNotApplicable", "That ticket choice is not available for the selected seat.") : labels.length ? labels.length === 1 ? t2("picker.labelsNoLongerAvailable.one", { labels: labels.join(", ") }) : t2("picker.labelsNoLongerAvailable.other", { labels: labels.join(", ") }) : this.tf("picker.seatsJustTaken", "One or more seats were just taken. Please pick again.");
6499
6941
  this.toast(message, "error");
6500
6942
  this.setCtaPhase("idle");
6501
6943
  } finally {
@@ -6824,13 +7266,14 @@ var SeatPicker = class _SeatPicker {
6824
7266
  const isBooth = details.objectType === "booth";
6825
7267
  const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
6826
7268
  const grid = isGroupedTable ? `<div class="sl-tip-grid"><div class="sl-tip-field"><span class="sl-tip-key">${esc(this.rowTypeWord(details))}</span><span class="sl-tip-val">${esc(details.rowLabel ?? details.displayLabel ?? details.label)}</span></div><div class="sl-tip-field"><span class="sl-tip-key">${this.tf("picker.guests", "Guests")}</span><span class="sl-tip-val">${details.bookingMode === "variable" ? `${details.minOccupancy}\u2013${details.maxOccupancy}` : details.capacity}</span></div></div>` : isBooth ? `<div class="sl-tip-grid">` + (details.sectionLabel ? `<div class="sl-tip-field"><span class="sl-tip-key">${this.tf("picker.section", "Section")}</span><span class="sl-tip-val">${esc(details.sectionLabel)}</span></div>` : "") + `<div class="sl-tip-field"><span class="sl-tip-key">${esc(this.rowTypeWord(details))}</span><span class="sl-tip-val">${esc(details.rowLabel ?? details.displayLabel ?? details.label)}</span></div></div>` : hasLoc ? `<div class="sl-tip-grid">` + (details.sectionLabel ? `<div class="sl-tip-field"><span class="sl-tip-key">${this.tf("picker.section", "Section")}</span><span class="sl-tip-val">${esc(details.sectionLabel)}</span></div>` : "") + (details.rowLabel ? `<div class="sl-tip-field"><span class="sl-tip-key">${esc(this.rowTypeWord(details))}</span><span class="sl-tip-val">${esc(this.rowShort(details))}</span></div>` : "") + (details.seatNumber ? `<div class="sl-tip-field"><span class="sl-tip-key">${this.tf("picker.seat", "Seat")}</span><span class="sl-tip-val">${esc(details.seatNumber)}</span></div>` : "") + `</div>` : `<div class="sl-tip-grid one"><div class="sl-tip-field"><span class="sl-tip-key">${this.tf("picker.seat", "Seat")}</span><span class="sl-tip-val">${esc(details.displayLabel ?? details.label)}</span></div></div>`;
6827
- const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
7269
+ const notForSale = details.status === "not_for_sale";
7270
+ const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${notForSale ? this.tf("picker.notAvailable", "Not available") : details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
6828
7271
  const limited = this.limitedViewLabel(details.commercial);
6829
7272
  const cxLine = limited ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u25D0</span>${esc(limited)}</div>` : "";
6830
7273
  const wheelchair = this.wheelchairProvisionLabel(details.wheelchairSpaceType);
6831
7274
  const wheelchairLine = wheelchair ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u267F</span>${esc(wheelchair)}</div>` : "";
6832
7275
  this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
6833
- this.tipEl.innerHTML = grid + `<div class="sl-tip-cat"><span class="sl-tip-dot" style="background:${details.categoryColor}"></span><span class="sl-tip-name">${esc(details.categoryLabel)}</span><span class="sl-tip-amt">${price}</span></div>` + wheelchairLine + cxLine + statusLine;
7276
+ this.tipEl.innerHTML = grid + `<div class="sl-tip-cat"><span class="sl-tip-dot" style="background:${details.categoryColor}"></span><span class="sl-tip-name">${esc(details.categoryLabel)}</span>` + (notForSale ? "" : `<span class="sl-tip-amt">${price}</span>`) + `</div>` + wheelchairLine + cxLine + statusLine;
6834
7277
  this.tipEl.style.display = "block";
6835
7278
  this.placeTooltip();
6836
7279
  }
@@ -7053,13 +7496,16 @@ var SeatPicker = class _SeatPicker {
7053
7496
  sourceLabel: this.tf("picker.chartDerivedModel", "Chart-derived model")
7054
7497
  };
7055
7498
  }
7056
- /** Route the module's decoupled analytics into the host callback, tagged buyer. */
7057
- emit3dAnalytics(event, props) {
7499
+ /** Route decoupled analytics into the host callback, tagged buyer. */
7500
+ emitAnalytics(event, props) {
7058
7501
  try {
7059
7502
  this.opts.onAnalytics?.(event, { ...props, surface: "buyer" });
7060
7503
  } catch {
7061
7504
  }
7062
7505
  }
7506
+ emit3dAnalytics(event, props) {
7507
+ this.emitAnalytics(event, props);
7508
+ }
7063
7509
  /** A 3D seat tap runs the SAME selection path as a 2D tap: toggle through the
7064
7510
  * controller, then raise the shared confirm card (bottom-sheeted in 3D). */
7065
7511
  onView3dSeatPick(seatId) {
@@ -7078,9 +7524,9 @@ var SeatPicker = class _SeatPicker {
7078
7524
  this.syncSelectionTo3d();
7079
7525
  return;
7080
7526
  }
7081
- const visualState = this.seatState3dFor(seat);
7082
- if (visualState !== "available") {
7083
- this.showUnavailable3dSeat(seat, visualState);
7527
+ const status = this.controller.getStatus(seat.id);
7528
+ if (status === "held" || status === "booked" || status === "not_for_sale") {
7529
+ this.showUnavailable3dSeat(seat, this.seatState3dFor(seat), status);
7084
7530
  this.syncSelectionTo3d();
7085
7531
  return;
7086
7532
  }
@@ -7102,14 +7548,14 @@ var SeatPicker = class _SeatPicker {
7102
7548
  else this.syncTray();
7103
7549
  this.syncSelectionTo3d();
7104
7550
  }
7105
- /** Explain a visible-but-unselectable 3D seat without entering the booking
7106
- * flow. Category colour remains visible in the venue; this card names the
7107
- * availability state explicitly so yellow never has to carry both meanings. */
7108
- showUnavailable3dSeat(seat, visualState) {
7551
+ /** Explain a 3D seat that INVENTORY refuses — never one the buyer's own
7552
+ * filters merely dimmed, which stays buyable exactly as it is in 2D. Category
7553
+ * colour remains visible in the venue; this card names the availability state
7554
+ * explicitly so yellow never has to carry both meanings. */
7555
+ showUnavailable3dSeat(seat, visualState, status) {
7109
7556
  const overlay = this.view3dEl;
7110
7557
  if (!overlay) return;
7111
7558
  this.dismissUnavailable3dSeat();
7112
- const status = this.controller.getStatus(seat.id);
7113
7559
  const details = this.controller.seatDetails(seat.id);
7114
7560
  const identity = details?.displayLabel ?? seat.displayLabel ?? seat.label;
7115
7561
  const section = details?.sectionLabel;
@@ -7121,12 +7567,9 @@ var SeatPicker = class _SeatPicker {
7121
7567
  } : status === "booked" ? {
7122
7568
  title: this.tf("picker.sold", "Sold"),
7123
7569
  message: this.tf("picker.soldSeatExplanation", "This seat has already been booked.")
7124
- } : status === "not_for_sale" ? {
7570
+ } : {
7125
7571
  title: this.tf("picker.notForSale", "Not for sale"),
7126
7572
  message: this.tf("picker.notForSaleExplanation", "This seat is not included in the current sale.")
7127
- } : {
7128
- title: this.tf("picker.filteredSeat", "Unavailable with current filters"),
7129
- message: this.tf("picker.filteredSeatExplanation", "Change the active price or view filters to make this seat selectable.")
7130
7573
  };
7131
7574
  const card = document.createElement("div");
7132
7575
  card.className = "sl-view3d-unavailable";
@@ -7504,7 +7947,7 @@ var SeatPicker = class _SeatPicker {
7504
7947
  const floors = handle.floors();
7505
7948
  const floorLabels = new Set(floors.map((floor) => floor.label.trim().toLocaleLowerCase()));
7506
7949
  const zones = handle.zones().filter((zone) => zone.seatCount > 0 && !floorLabels.has(zone.label.trim().toLocaleLowerCase()));
7507
- const allSections = handle.sections().filter((s) => s.seatCount > 0);
7950
+ const allSections = handle.sections().filter((section) => section.seatCount > 0 && !floorLabels.has(section.label.trim().toLocaleLowerCase()));
7508
7951
  const sections = zones.length > 1 ? [] : allSections;
7509
7952
  const wantFloors = floors.length > 1;
7510
7953
  const wantZones = zones.length > 1;
@@ -7531,16 +7974,24 @@ var SeatPicker = class _SeatPicker {
7531
7974
  });
7532
7975
  setFinderOpen(false);
7533
7976
  nav.appendChild(finderToggle);
7977
+ let syncAreasForFloor = (_index) => void 0;
7978
+ let syncPressedFloor = (_index) => void 0;
7534
7979
  if (wantFloors) {
7535
7980
  const row = document.createElement("div");
7536
7981
  row.setAttribute("role", "group");
7537
7982
  row.setAttribute("aria-label", this.tf("picker.levels", "Levels"));
7538
7983
  const pills = [];
7984
+ syncPressedFloor = (index) => {
7985
+ pills.forEach((pill) => {
7986
+ pill.setAttribute("aria-pressed", String(
7987
+ (pill.dataset.floor === "" ? null : Number(pill.dataset.floor)) === index
7988
+ ));
7989
+ });
7990
+ };
7539
7991
  const select = (index) => {
7540
7992
  if (!handle.focusFloor(index)) return;
7541
- pills.forEach((p) => {
7542
- p.setAttribute("aria-pressed", String((p.dataset.floor === "" ? null : Number(p.dataset.floor)) === index));
7543
- });
7993
+ syncPressedFloor(index);
7994
+ syncAreasForFloor(index);
7544
7995
  };
7545
7996
  const add = (label, index) => {
7546
7997
  const b = document.createElement("button");
@@ -7565,39 +8016,55 @@ var SeatPicker = class _SeatPicker {
7565
8016
  row.setAttribute("aria-label", this.tf("picker.areas", "Areas"));
7566
8017
  const entries = wantZones ? zones.map((z) => ({ id: z.id, label: z.label || z.id, go: () => {
7567
8018
  handle.focusZone(z.id);
7568
- } })) : sections.map((s) => ({ id: s.id, label: s.label || s.id, go: () => {
7569
- handle.focusSection(s.id);
7570
- } }));
7571
- if (!wantZones && entries.length > MAX_3D_SECTION_PILLS) {
7572
- const select = document.createElement("select");
7573
- select.setAttribute("aria-label", this.tf("picker.jumpToSection", "Jump to section"));
7574
- const placeholder = document.createElement("option");
7575
- placeholder.value = "";
7576
- placeholder.textContent = this.tf("picker.jumpToSection", "Jump to section");
7577
- select.appendChild(placeholder);
7578
- for (const entry of entries) {
7579
- const option = document.createElement("option");
7580
- option.value = entry.id;
7581
- option.textContent = entry.label;
7582
- select.appendChild(option);
8019
+ } })) : sections.map((s) => ({
8020
+ id: s.id,
8021
+ label: s.label || s.id,
8022
+ floorIndex: s.floorIndex,
8023
+ go: () => {
8024
+ handle.focusSection(s.id);
8025
+ if (Number.isFinite(s.floorIndex)) syncPressedFloor(s.floorIndex);
7583
8026
  }
7584
- select.addEventListener("change", () => {
7585
- entries.find((entry) => entry.id === select.value)?.go();
7586
- });
7587
- row.appendChild(select);
7588
- } else {
7589
- for (const e of entries) {
7590
- const b = document.createElement("button");
7591
- b.type = "button";
7592
- b.textContent = e.label;
7593
- b.addEventListener("click", () => {
7594
- e.go();
8027
+ }));
8028
+ const renderEntries = (floorIndex) => {
8029
+ const visible = wantZones || floorIndex === null ? entries : entries.filter((entry) => entry.floorIndex === void 0 || entry.floorIndex === floorIndex);
8030
+ row.replaceChildren();
8031
+ if (!visible.length) {
8032
+ row.remove();
8033
+ return;
8034
+ }
8035
+ if (!row.isConnected) nav.appendChild(row);
8036
+ if (!wantZones && visible.length > MAX_3D_SECTION_PILLS) {
8037
+ const select = document.createElement("select");
8038
+ select.setAttribute("aria-label", this.tf("picker.jumpToSection", "Jump to section"));
8039
+ const placeholder = document.createElement("option");
8040
+ placeholder.value = "";
8041
+ placeholder.textContent = this.tf("picker.jumpToSection", "Jump to section");
8042
+ select.appendChild(placeholder);
8043
+ for (const entry of visible) {
8044
+ const option = document.createElement("option");
8045
+ option.value = entry.id;
8046
+ option.textContent = entry.label;
8047
+ select.appendChild(option);
8048
+ }
8049
+ select.addEventListener("change", () => {
8050
+ visible.find((entry) => entry.id === select.value)?.go();
8051
+ });
8052
+ row.appendChild(select);
8053
+ return;
8054
+ }
8055
+ for (const entry of visible) {
8056
+ const button = document.createElement("button");
8057
+ button.type = "button";
8058
+ button.textContent = entry.label;
8059
+ button.addEventListener("click", () => {
8060
+ entry.go();
7595
8061
  setFinderOpen(false);
7596
8062
  });
7597
- row.appendChild(b);
8063
+ row.appendChild(button);
7598
8064
  }
7599
- }
7600
- nav.appendChild(row);
8065
+ };
8066
+ syncAreasForFloor = renderEntries;
8067
+ renderEntries(null);
7601
8068
  }
7602
8069
  overlay.appendChild(nav);
7603
8070
  }
@@ -7796,7 +8263,7 @@ var SeatPicker = class _SeatPicker {
7796
8263
  } catch (err) {
7797
8264
  this.opts.onError?.(err);
7798
8265
  const reason = err?.reason;
7799
- const message = reason === "not_enough_together" ? t2("picker.couldNotFindSeatsTogether", { count: qty }) : reason === "sold_out" ? this.tf("picker.ticketTypeSoldOut", "That ticket type is sold out. Try another ticket type.") : reason === "event_closed" ? this.tf("picker.seatSalesClosedForEvent", "Seat sales have closed for this event.") : this.tf("picker.seatsNoLongerAvailableTryAnother", "Those seats are no longer available. Try another quantity or ticket type.");
8266
+ const message = reason === "not_enough_together" ? t2("picker.couldNotFindSeatsTogether", { count: qty }) : reason === "companion_pair_required" ? this.tf("picker.companionPairRequired", "Select the adjacent wheelchair place with each companion ticket.") : reason === "ticket_option_not_applicable" ? this.tf("picker.ticketOptionNotApplicable", "That ticket choice is not available for the selected seat.") : reason === "sold_out" ? this.tf("picker.ticketTypeSoldOut", "That ticket type is sold out. Try another ticket type.") : reason === "event_closed" ? this.tf("picker.seatSalesClosedForEvent", "Seat sales have closed for this event.") : this.tf("picker.seatsNoLongerAvailableTryAnother", "Those seats are no longer available. Try another quantity or ticket type.");
7800
8267
  this.toast(message, "error");
7801
8268
  return null;
7802
8269
  } finally {