@seatlayer/js 0.50.2 → 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",
@@ -2248,6 +2255,16 @@ var DEFAULT_MAX_SELECTION2 = 10;
2248
2255
  var EXTEND_PROMPT_MS = 6e4;
2249
2256
  var MAX_3D_SEATS_DEFAULT = 6e4;
2250
2257
  var MAX_3D_SECTION_PILLS = 12;
2258
+ function tfStatic(key, fallback) {
2259
+ const v = t2(key);
2260
+ return v === key ? fallback : v;
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
+ }
2251
2268
  function pointInPolygon(x, y, poly) {
2252
2269
  let inside = false;
2253
2270
  for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
@@ -2350,9 +2367,13 @@ var CSS = (
2350
2367
  the WIDGET's meanings, not an org's brand: premium gold, environment
2351
2368
  warning, danger, success. Hosts can still override them with CSS. */
2352
2369
  --sl-premium:#e8c15a;--sl-premium-deep:#c9a24b;--sl-premium-ink:#1c1608;
2353
- --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}
2354
2373
  .sl-picker *{box-sizing:border-box;margin:0;padding:0}
2355
- .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}
2356
2377
 
2357
2378
  /* header */
2358
2379
  .sl-head{display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--sl-line);flex:none}
@@ -2422,11 +2443,30 @@ var CSS = (
2422
2443
  .sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
2423
2444
  .sl-picker[data-layout="narrow"] .sl-side{width:100%;border-left:0;border-top:1px solid var(--sl-line);
2424
2445
  flex:none;height:min(72%,480px);overflow:hidden;transition:height .3s cubic-bezier(.2,.8,.2,1);overscroll-behavior:contain}
2425
- .sl-picker[data-layout="narrow"][data-sheet="open"][data-has-selection="false"] .sl-side{height:min(252px,52%)}
2446
+ .sl-picker[data-layout="narrow"][data-sheet="open"][data-has-selection="false"] .sl-side{height:min(380px,64%)}
2447
+ /* With the price list expanded the sheet takes the with-selection height so
2448
+ the tray (cart + best-seats) is never crushed under the fixed-size list. */
2449
+ .sl-picker[data-layout="narrow"][data-sheet="open"][data-prices-open="true"] .sl-side{height:min(480px,74%)}
2426
2450
  .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side{height:76px;overflow:hidden}
2427
2451
  .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
2428
- .sl-picker[data-layout="narrow"] .sl-tray{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain}
2452
+ .sl-picker[data-layout="narrow"] .sl-tray{flex:1;min-height:96px;overflow-y:auto;overscroll-behavior:contain}
2453
+ .sl-picker[data-layout="narrow"][data-prices-open="true"] .sl-tray{min-height:48px}
2454
+ .sl-picker[data-layout="narrow"] .sl-prices{max-height:150px;overflow-y:auto;overscroll-behavior:contain}
2429
2455
  .sl-picker[data-layout="narrow"] .sl-foot{position:static;background:var(--sl-bg)}
2456
+ /* NARROW FOOT SAYS EVERYTHING ONCE (owner report: with a hold the footer ate
2457
+ ~190px and the ticket list got a sliver). The sheet head already carries
2458
+ "N tickets \xB7 $total" and the header pill already ticks the clock \u2014 so the
2459
+ total row hides, the secured note collapses to one compact line, and the
2460
+ badge tucks in. The tray gets the pixels; a thin scrollbar shows there is
2461
+ more to scroll. */
2462
+ .sl-picker[data-layout="narrow"] .sl-total{display:none}
2463
+ .sl-picker[data-layout="narrow"] .sl-foot{padding:8px 12px 8px}
2464
+ .sl-picker[data-layout="narrow"] .sl-hold-note{margin-bottom:6px;padding:5px 9px}
2465
+ .sl-picker[data-layout="narrow"] .sl-hold-note svg{display:none}
2466
+ .sl-picker[data-layout="narrow"] .sl-hold-note b{display:inline;margin-right:6px;font-size:11px}
2467
+ .sl-picker[data-layout="narrow"] .sl-hold-copy{display:inline;white-space:normal;font-size:10.5px}
2468
+ .sl-picker[data-layout="narrow"] .sl-powered{margin-top:4px;padding:2px 8px;font-size:11px}
2469
+ .sl-picker[data-layout="narrow"] .sl-tray{scrollbar-width:thin}
2430
2470
  .sl-picker[data-layout="narrow"] .sl-foot.empty{display:none}
2431
2471
  .sl-picker[data-layout="narrow"] .sl-sheet-head{order:0}
2432
2472
  .sl-picker[data-layout="narrow"] .sl-seats-sec{display:none}
@@ -2434,6 +2474,7 @@ var CSS = (
2434
2474
  .sl-picker[data-layout="narrow"] .sl-filtersec{order:3}
2435
2475
  .sl-picker[data-layout="narrow"] .sl-filters{order:4}
2436
2476
  .sl-picker[data-layout="narrow"] .sl-prices-sec{order:5}
2477
+ .sl-picker[data-layout="narrow"] .sl-prices-hint{order:6}
2437
2478
  .sl-picker[data-layout="narrow"] .sl-pricef{order:6}
2438
2479
  .sl-picker[data-layout="narrow"] .sl-prices{order:7}
2439
2480
  .sl-picker[data-layout="narrow"] .sl-foot{order:8}
@@ -2441,6 +2482,7 @@ var CSS = (
2441
2482
  .sl-picker[data-layout="narrow"] .sl-filtersec,
2442
2483
  .sl-picker[data-layout="narrow"] .sl-filters,
2443
2484
  .sl-picker[data-layout="narrow"] .sl-prices-sec,
2485
+ .sl-picker[data-layout="narrow"] .sl-prices-hint,
2444
2486
  .sl-picker[data-layout="narrow"] .sl-prices{display:none!important}
2445
2487
  .sl-picker[data-layout="narrow"][data-has-selection="true"] .sl-filtersec,
2446
2488
  .sl-picker[data-layout="narrow"][data-has-selection="true"] .sl-filters,
@@ -2458,7 +2500,13 @@ var CSS = (
2458
2500
  head is the tap/swipe toggle target (min 44px), so it reads as one control. */
2459
2501
  .sl-sheet-head{display:none;flex-direction:column;justify-content:center;padding:6px 12px 8px;min-height:56px;
2460
2502
  cursor:pointer;touch-action:none;user-select:none;-webkit-user-select:none;flex:none}
2461
- .sl-picker[data-layout="narrow"] .sl-sheet-head{display:flex;min-height:64px;padding:4px 10px 6px}
2503
+ .sl-picker[data-layout="narrow"] .sl-sheet-head{display:flex;min-height:52px;padding:3px 10px 5px}
2504
+ /* OPEN sheet: the head is context, not controls \u2014 the pill it carries at peek
2505
+ ("Best seats"/"Continue") duplicates the card and CTA right below it, and
2506
+ every saved pixel is tray room (owner call 2026-08-14). */
2507
+ .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-sheet-head{min-height:42px}
2508
+ .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-sheet-bar{min-height:32px}
2509
+ .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-sheet-peek .go{display:none}
2462
2510
  .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-sheet-head{height:100%}
2463
2511
  .sl-sheet-grab{width:36px;height:4px;border-radius:999px;background:var(--sl-muted);opacity:.55;margin:1px auto 5px}
2464
2512
  .sl-sheet-bar{display:flex;align-items:center;gap:8px;min-height:44px}
@@ -2499,9 +2547,19 @@ var CSS = (
2499
2547
  sheet brings it back, exactly like the filters row above. The tray hint comes
2500
2548
  back with it: the first-time buyer on a phone needs the instruction more than
2501
2549
  the desktop buyer, not less. */
2502
- .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-prices-sec{display:flex!important}
2503
- .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-prices{display:flex!important}
2550
+ .sl-picker[data-layout="narrow"][data-sheet="open"][data-prices-open="true"] .sl-prices-sec{display:flex!important}
2551
+ /* MOBILE SPACE BELONGS TO THE CART (owner call 2026-08-14): the price list is
2552
+ one tap away behind the "Ticket prices" row, not a permanent tenant. Zone
2553
+ flags and section taps already answer "what does it cost" in context. */
2554
+ .sl-picker[data-layout="narrow"][data-sheet="open"][data-prices-open="true"] .sl-prices-hint{display:block!important}
2555
+ .sl-picker[data-layout="narrow"][data-sheet="open"][data-prices-open="true"] .sl-prices{display:flex!important}
2504
2556
  .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-tray-hint{display:block!important}
2557
+ /* The row reads as the toggle it is on narrow: chevron + pointer. */
2558
+ .sl-picker[data-layout="narrow"] .sl-prices-sec{cursor:pointer}
2559
+ .sl-picker[data-layout="narrow"] .sl-prices-sec::after{content:'';width:8px;height:8px;flex:none;margin-left:auto;
2560
+ border-right:2px solid var(--sl-muted);border-bottom:2px solid var(--sl-muted);
2561
+ transform:rotate(45deg);transition:transform .2s cubic-bezier(.2,.8,.2,1)}
2562
+ .sl-picker[data-layout="narrow"][data-prices-open="true"] .sl-prices-sec::after{transform:rotate(-135deg)}
2505
2563
  .sl-cbbtn{width:32px;height:32px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
2506
2564
  color:var(--sl-text);display:flex;align-items:center;justify-content:center;transition:border-color .15s}
2507
2565
  .sl-cbbtn:hover{border-color:var(--sl-muted)}
@@ -2511,24 +2569,30 @@ var CSS = (
2511
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));
2512
2570
  border-radius:12px;background:color-mix(in srgb,var(--sl-accent) 8%,var(--sl-surface));color:var(--sl-text)}
2513
2571
  .sl-offer.has{display:block}.sl-offer-main{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}
2514
- .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)}
2515
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)}
2516
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;
2517
2575
  display:grid;place-items:center;cursor:pointer;font-size:12px;font-weight:850;color:var(--sl-text);background:var(--sl-surface)}
2518
- .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)}
2519
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)}
2520
2578
  .sl-sec{padding:14px 14px 4px;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:700}
2521
2579
  .sl-prices-sec{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:13px}
2522
2580
  .sl-price-select{min-height:32px;max-width:130px;padding:5px 28px 5px 9px;border:1px solid var(--sl-line);border-radius:9px;
2523
- background:var(--sl-surface);color:var(--sl-text);font:inherit;font-size:11px;font-weight:750;letter-spacing:0;text-transform:none}
2581
+ background-color:var(--sl-surface);color:var(--sl-text);font:inherit;font-size:11px;font-weight:750;letter-spacing:0;text-transform:none}
2582
+ /* One sentence that teaches the whole colour code \u2014 a first-time buyer sees
2583
+ "Head Tables \xB7 $165" and coloured dots with nothing connecting them. Gone
2584
+ the moment they have seats (they've learned it) and while sales are closed. */
2585
+ .sl-prices-hint{padding:0 14px 7px;font-size:11px;line-height:1.45;color:var(--sl-muted)}
2586
+ .sl-picker[data-has-selection="true"] .sl-prices-hint,
2587
+ .sl-picker[data-sales-closed="true"] .sl-prices-hint{display:none}
2524
2588
  .sl-prices{display:flex;flex-direction:column;padding:4px 14px 8px;border-bottom:1px solid var(--sl-line)}
2525
2589
  .sl-prices-sec,.sl-prices,.sl-seats-sec{flex:none}
2526
2590
  .sl-price-row{display:flex;align-items:center;gap:7px;min-height:28px;font-size:12px;
2527
2591
  padding:0 6px;margin:0 -6px;border-radius:8px;cursor:pointer;transition:background .15s}
2528
2592
  .sl-price-row:hover,.sl-price-row:focus-visible{background:color-mix(in srgb,var(--sl-line) 40%,transparent)}
2529
2593
  .sl-price-row.sl-active{background:color-mix(in srgb,var(--sl-accent) 9%,transparent)}
2530
- .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}
2531
- .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)}
2532
2596
  .sl-dot{width:9px;height:9px;border-radius:50%;flex:none}
2533
2597
  .sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
2534
2598
  .sl-price-left{font-size:11px;color:var(--sl-muted);font-variant-numeric:tabular-nums}
@@ -2563,7 +2627,7 @@ var CSS = (
2563
2627
  box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
2564
2628
  .sl-ticket-state{width:17px;height:17px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
2565
2629
  background:var(--sl-accent);color:var(--sl-accent-ink)}
2566
- .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)}
2567
2631
  .sl-ticket-state svg{width:10px;height:10px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
2568
2632
  .sl-chip-main{min-width:0;padding:8px 10px 8px 11px;display:flex;flex-direction:column;justify-content:center;gap:5px}
2569
2633
  .sl-chip-id{display:flex;gap:12px;min-width:0}
@@ -2606,7 +2670,7 @@ var CSS = (
2606
2670
  .sl-ga-qty span{min-width:16px;text-align:center;font-weight:800;font-variant-numeric:tabular-nums}
2607
2671
 
2608
2672
  /* footer */
2609
- .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;
2610
2674
  background:var(--sl-bg);box-shadow:0 -10px 24px -22px rgba(0,0,0,.72)}
2611
2675
  .sl-hold-note{display:none;align-items:center;gap:7px;margin-bottom:8px;padding:7px 8px;border-radius:var(--sl-r-sm);
2612
2676
  border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
@@ -2620,14 +2684,13 @@ var CSS = (
2620
2684
  .sl-hold-note>span{flex:1;min-width:0}
2621
2685
  .sl-hold-change{flex:none;min-height:30px;padding:5px 8px;border-radius:8px;border:1px solid var(--sl-line);
2622
2686
  color:var(--sl-text);font-size:10.5px;font-weight:750;white-space:nowrap}
2623
- .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)}
2624
2688
  .sl-hold-change:disabled{opacity:.58;cursor:wait}
2625
2689
  .sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
2626
2690
  .sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
2627
2691
  .sl-value-pop{animation:slValuePop .32s cubic-bezier(.2,.8,.2,1)}
2628
- /* Primary checkout CTA. Scoped under .sl-picker so it OUTWEIGHS the
2629
- '.sl-picker button' reset (0,1,1) \u2014 an unscoped '.sl-cta' (0,1,0) loses to it
2630
- 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. */
2631
2694
  .sl-picker .sl-cta{display:flex;align-items:center;justify-content:center;width:100%;min-height:44px;
2632
2695
  padding:12px 16px;border-radius:var(--sl-r-sm);font-weight:800;font-size:14px;line-height:1.1;
2633
2696
  background:var(--sl-accent);color:var(--sl-accent-ink);
@@ -2638,8 +2701,8 @@ var CSS = (
2638
2701
  .sl-cta-spin,.sl-ba-spin{width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;
2639
2702
  animation:slspin .7s linear infinite;flex:none}
2640
2703
  /* Disabled ("Select seats"): quieter, but still a full-width button shape. */
2641
- .sl-picker .sl-cta:disabled{background:var(--sl-surface);color:var(--sl-muted);opacity:1;
2642
- 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}
2643
2706
 
2644
2707
  /* Chrome anchor regions (Feature 6) \u2014 every interactive map overlay is APPENDED
2645
2708
  INTO one of these positioned flex containers and flows/stacks within it, so no
@@ -2669,7 +2732,7 @@ var CSS = (
2669
2732
  in the same region, so no displacement hack is needed. */
2670
2733
  .sl-testbadge{display:inline-flex;align-items:center;padding:5px 10px;border-radius:7px;pointer-events:none;
2671
2734
  font-size:10px;font-weight:850;letter-spacing:.13em;line-height:1.2;text-transform:uppercase;white-space:nowrap;
2672
- 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));
2673
2736
  border:1px dashed color-mix(in srgb,var(--sl-warn) 55%,transparent)}
2674
2737
 
2675
2738
  /* zoom column (flows within the bottom-right region) */
@@ -2680,11 +2743,11 @@ var CSS = (
2680
2743
  color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
2681
2744
  .sl-zoom button:hover{border-color:var(--sl-muted)}
2682
2745
  .sl-zoom svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
2683
- /* Full-screen promotion on phones: the capability existed (native + iOS overlay
2684
- + framed-host paths) but hid as the 4th unlabelled glyph in a corner stack.
2685
- On narrow layouts dockLayoutChrome moves the SAME button into the top-right
2686
- region beside Map|3D and this class dresses it as a labelled pill \u2014 the one
2687
- accelerator with the highest payoff on a small screen gets words. */
2746
+ /* Full-screen promotion: the capability existed (native + iOS overlay +
2747
+ framed-host paths) but hid as the 4th unlabelled glyph in a corner stack \u2014
2748
+ even the owner could not pick it out. dockLayoutChrome docks the SAME
2749
+ button into the top-right region beside Map|3D on EVERY layout and this
2750
+ class dresses it as a labelled pill. */
2688
2751
  .sl-zfs-lbl{display:none}
2689
2752
  .sl-fs-pill.sl-fs-pill{display:inline-flex;align-items:center;justify-content:center;width:auto;min-height:40px;height:auto;
2690
2753
  padding:0 13px;gap:6px;border-radius:999px;font-size:11px;font-weight:800;
@@ -2692,6 +2755,16 @@ var CSS = (
2692
2755
  .sl-fs-pill.sl-fs-pill:hover{border-color:var(--sl-muted)}
2693
2756
  .sl-fs-pill svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
2694
2757
  .sl-fs-pill .sl-zfs-lbl{display:inline;letter-spacing:.02em;white-space:nowrap}
2758
+ /* In the header, icons carry it (owner call): square Full screen glyph, and
2759
+ on narrow a price-tag toggle that opens the sheet with the list expanded. */
2760
+ .sl-head .sl-fs-pill{min-height:34px;min-width:34px;width:34px;padding:0;flex:none}
2761
+ .sl-head .sl-fs-pill .sl-zfs-lbl{display:none}
2762
+ .sl-prices-btn{display:none;align-items:center;justify-content:center;width:34px;min-height:34px;flex:none;
2763
+ border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-text);transition:border-color .15s}
2764
+ .sl-prices-btn:hover{border-color:var(--sl-muted)}
2765
+ .sl-prices-btn svg{width:15px;height:15px;stroke:currentColor;stroke-width:1.8;fill:none;stroke-linecap:round;stroke-linejoin:round}
2766
+ .sl-prices-btn[aria-pressed="true"]{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
2767
+ .sl-picker[data-layout="narrow"] .sl-prices-btn{display:inline-flex}
2695
2768
 
2696
2769
  /* toast + boot states (toast flows in the bottom-center region) */
2697
2770
  .sl-toast{transform:translateY(6px) scale(.98);max-width:100%;
@@ -2726,7 +2799,7 @@ var CSS = (
2726
2799
  .sl-extend-btn{flex:none;padding:8px 14px;border-radius:999px;font-weight:800;font-size:12.5px;
2727
2800
  background:var(--sl-accent);color:var(--sl-accent-ink);transition:filter .15s,opacity .15s}
2728
2801
  .sl-extend-btn:hover{filter:brightness(1.08)}
2729
- .sl-extend-btn:disabled{opacity:.5;cursor:not-allowed}
2802
+ .sl-extend-btn:disabled{opacity:.62;cursor:not-allowed}
2730
2803
 
2731
2804
  /* booked confirmation overlay (covers the widget once the held seats are sold) */
2732
2805
  .sl-booked{position:absolute;inset:0;z-index:11;display:flex;flex-direction:column;align-items:center;
@@ -2753,7 +2826,7 @@ var CSS = (
2753
2826
  justify-content:center;text-align:center;gap:8px;padding:24px;
2754
2827
  background:color-mix(in srgb,var(--sl-bg) 82%,transparent);backdrop-filter:blur(4px)}
2755
2828
  .sl-soldout.on{display:flex}
2756
- .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}
2757
2830
  .sl-soldout-title{font-size:32px;font-weight:800;color:var(--sl-text);line-height:1.05}
2758
2831
  .sl-soldout-copy{max-width:360px;font-size:13px;color:var(--sl-muted);line-height:1.5}
2759
2832
 
@@ -2769,9 +2842,9 @@ var CSS = (
2769
2842
  /* "Powered by SeatLayer" attribution badge (side-panel foot) \u2014 the canonical
2770
2843
  Layered Rows mark + wordmark. Hidden when the host opts out or the org's paid
2771
2844
  theme sets hideBadge. */
2772
- .sl-powered{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:10px;
2773
- font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--sl-text);opacity:.72;
2774
- 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;
2775
2848
  transition:opacity .15s ease,background-color .15s ease}
2776
2849
  .sl-powered:hover{opacity:1;background:color-mix(in srgb,var(--sl-text) 8%,transparent)}
2777
2850
  .sl-powered:focus-visible{opacity:1;outline:2px solid var(--sl-accent);outline-offset:2px}
@@ -2809,18 +2882,44 @@ var CSS = (
2809
2882
  .sl-confirm-field:first-child .sl-confirm-value{font-size:13.5px;line-height:1.25;white-space:normal;
2810
2883
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
2811
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))}
2812
- .sl-confirm-cat .sl-dot{border:2px solid rgba(255,255,255,.78);width:11px;height:11px}
2813
- .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}
2814
- .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}
2815
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}
2816
2901
  .sl-confirm-row{display:flex;gap:8px;margin-top:10px}
2817
2902
  .sl-confirm-row button{flex:1;min-height:44px;padding:9px 12px;border-radius:9px;font-weight:800;font-size:13px}
2818
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}
2819
2904
  .sl-confirm-add svg{width:16px;height:16px;stroke:currentColor;stroke-width:2.8;fill:none;stroke-linecap:round;stroke-linejoin:round}
2820
2905
  .sl-confirm-cancel{background:color-mix(in srgb,var(--sl-line) 44%,transparent)!important;border:1px solid var(--sl-line)!important;color:var(--sl-muted)!important}
2821
2906
  .sl-confirm-cancel:hover{color:var(--sl-text)}
2822
- .sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
2907
+ .sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(310px,calc(100% - 24px));
2823
2908
  transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
2909
+ /* Phone-size confirm card (owner call 2026-08-14): same content \u2014 identity,
2910
+ price band, view photo, 3D link \u2014 on a tighter grid. The values stay large
2911
+ enough to double-check against the map; buttons keep tap-size. */
2912
+ .sl-picker[data-layout="narrow"] .sl-confirm-field{padding:9px 9px 8px}
2913
+ .sl-picker[data-layout="narrow"] .sl-confirm-value{font-size:15px}
2914
+ .sl-picker[data-layout="narrow"] .sl-confirm-cat{padding:8px 10px}
2915
+ .sl-picker[data-layout="narrow"] .sl-confirm-cat-name{font-size:12.5px}
2916
+ .sl-picker[data-layout="narrow"] .sl-confirm-price{font-size:15px}
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}
2919
+ .sl-picker[data-layout="narrow"] .sl-confirm-thumbwrap{height:62px;margin-bottom:6px}
2920
+ .sl-picker[data-layout="narrow"] .sl-confirm-3d{margin-top:7px;padding:6px}
2921
+ .sl-picker[data-layout="narrow"] .sl-confirm-row{margin-top:8px}
2922
+ .sl-picker[data-layout="narrow"] .sl-confirm-row button{min-height:42px}
2824
2923
 
2825
2924
  /* Atomic whole/variable-table chooser. Lives inside the widget so the same
2826
2925
  accessible dialog works in inline, modal, desktop and 390px mobile hosts. */
@@ -2840,7 +2939,7 @@ var CSS = (
2840
2939
  .sl-table-stepper{display:grid;grid-template-columns:48px 1fr 48px;align-items:center;border:1px solid var(--sl-line);
2841
2940
  border-radius:12px;overflow:hidden;background:var(--sl-bg)}
2842
2941
  .sl-table-stepper button{height:48px;font-size:24px;font-weight:700;background:color-mix(in srgb,var(--sl-line) 34%,transparent)!important}
2843
- .sl-table-stepper button:disabled{opacity:.38;cursor:not-allowed}
2942
+ .sl-table-stepper button:disabled{opacity:.58;cursor:not-allowed}
2844
2943
  .sl-table-stepper output{text-align:center;font-size:19px;font-weight:850;font-variant-numeric:tabular-nums}
2845
2944
  .sl-table-range{margin-top:7px;color:var(--sl-muted);font-size:11px;text-align:center}
2846
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}
@@ -2882,22 +2981,25 @@ var CSS = (
2882
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}
2883
2982
  .sl-ba-title,.sl-ba-copy,.sl-ba select,.sl-ba-qty,.sl-ba-go{position:relative;z-index:1}
2884
2983
  .sl-ba-title{grid-column:1/-1;display:flex;align-items:center;gap:7px;font-size:13px;font-weight:850}
2885
- .sl-ba-title .spark{color:var(--sl-accent);font-size:16px}
2984
+ .sl-ba-title .spark{color:var(--sl-accent-text);font-size:16px}
2886
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}
2887
2986
  .sl-ba-copy .narrow{display:none}
2888
2987
  /* "\u2605 Best seats" premium quick-pick \u2014 gold accent echoing the \u2605 Premium pill on
2889
2988
  the confirm popover; deliberately distinct from the accent-toned qty/go. */
2890
2989
  .sl-ba-premium{position:relative;z-index:1;grid-column:1/-1;justify-self:start;display:inline-flex;align-items:center;gap:6px;
2891
2990
  padding:6px 12px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.02em;cursor:pointer;
2892
- 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));
2893
2992
  border:1px solid color-mix(in srgb,var(--sl-premium) 34%,var(--sl-line));transition:filter .15s,background .15s,color .15s}
2894
2993
  .sl-ba-premium .star{font-size:12px;line-height:1;color:var(--sl-premium)}
2895
2994
  .sl-ba-premium:hover{filter:brightness(1.05)}
2896
2995
  .sl-ba-premium.on{color:var(--sl-premium-ink);background:linear-gradient(135deg,#f0cf6b,#e0b23f);border-color:transparent;
2897
2996
  box-shadow:0 6px 16px color-mix(in srgb,var(--sl-premium) 26%,transparent)}
2898
2997
  .sl-ba-premium.on .star{color:#5a4410}
2899
- .sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:8px;
2900
- font:inherit;font-size:11px;padding:7px 8px;min-width:0;width:100%;max-width:none}
2998
+ .sl-ba select,.sl-price-select{appearance:none;-webkit-appearance:none;
2999
+ background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23949ca9' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
3000
+ background-repeat:no-repeat;background-position:right 10px center}
3001
+ .sl-ba select{background-color:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:8px;
3002
+ font:inherit;font-size:11px;padding:7px 26px 7px 8px;min-width:0;width:100%;max-width:none}
2901
3003
  .sl-ba-qty{display:flex;align-items:center;gap:7px;padding:3px;border:1px solid var(--sl-line);border-radius:9px;background:var(--sl-surface)}
2902
3004
  .sl-ba-qty button{width:25px;height:25px;border-radius:7px;background:color-mix(in srgb,var(--sl-line) 35%,transparent);border:0;
2903
3005
  font-size:14px;font-weight:800;display:flex;align-items:center;justify-content:center}
@@ -2927,9 +3029,23 @@ var CSS = (
2927
3029
  .sl-picker[data-layout="narrow"] .sl-ba-copy{display:none}
2928
3030
  .sl-picker[data-layout="narrow"] .sl-ba-copy .wide{display:none}
2929
3031
  .sl-picker[data-layout="narrow"] .sl-ba-copy .narrow{display:inline}
2930
- .sl-picker[data-layout="narrow"] .sl-ba select{min-height:40px}
3032
+ .sl-picker[data-layout="narrow"] .sl-ba{grid-template-columns:auto minmax(0,1fr)}
3033
+ .sl-picker[data-layout="narrow"] .sl-ba select{grid-column:1/-1;min-height:38px}
2931
3034
  .sl-picker[data-layout="narrow"] .sl-ba-qty button{width:30px;height:30px}
2932
- .sl-picker[data-layout="narrow"] .sl-ba-go{min-height:40px}
3035
+ /* Quantity and Go share ONE row: a full-width row holding only "\u2212 2 +" was
3036
+ dead space, and the Find button still gets the lion's share. */
3037
+ .sl-picker[data-layout="narrow"] .sl-ba-qty{grid-column:1}
3038
+ .sl-picker[data-layout="narrow"] .sl-ba-go{grid-column:2;min-height:40px}
3039
+ /* Re-entry to best-available once the cart has seats (the card yields its
3040
+ space to chips, but the accelerator must stay reachable) + the quiet way
3041
+ back out of a reopened card. */
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;
3043
+ border:1px dashed color-mix(in srgb,var(--sl-accent) 40%,var(--sl-line));border-radius:999px;
3044
+ color:var(--sl-accent-text);font-size:11.5px;font-weight:750;transition:border-color .15s,background .15s}
3045
+ .sl-ba-reopen:hover,.sl-ba-reopen:focus-visible{background:color-mix(in srgb,var(--sl-accent) 8%,transparent)}
3046
+ .sl-ba-back{grid-column:1/-1;justify-self:start;min-height:30px;padding:3px 0;color:var(--sl-muted);
3047
+ font-size:11px;font-weight:700;text-decoration:underline;text-underline-offset:3px;transition:color .15s}
3048
+ .sl-ba-back:hover,.sl-ba-back:focus-visible{color:var(--sl-text)}
2933
3049
 
2934
3050
  /* Sales-closed is a designed state, not a set of disables: one statement in
2935
3051
  the tray where the buying flow was, carrying the event's own date so the
@@ -2948,9 +3064,9 @@ var CSS = (
2948
3064
  (the cart chip's remove/view rail) instead get real height on narrow. */
2949
3065
  @media (pointer:coarse){
2950
3066
  .sl-zoom button,.sl-cbbtn,.sl-close,.sl-ga-qty button,.sl-ba-qty button,.sl-price-more,
2951
- .sl-hold-change,.sl-seccard-x,.sl-fs-pill,.sl-side-toggle{position:relative}
3067
+ .sl-hold-change,.sl-seccard-x,.sl-fs-pill,.sl-side-toggle,.sl-ba-reopen{position:relative}
2952
3068
  .sl-zoom button::after,.sl-cbbtn::after,.sl-close::after,.sl-ga-qty button::after,.sl-ba-qty button::after,
2953
- .sl-price-more::after,.sl-hold-change::after,.sl-seccard-x::after,.sl-fs-pill::after,.sl-side-toggle::after{
3069
+ .sl-price-more::after,.sl-hold-change::after,.sl-seccard-x::after,.sl-fs-pill::after,.sl-side-toggle::after,.sl-ba-reopen::after{
2954
3070
  content:'';position:absolute;top:50%;left:50%;width:max(100%,44px);height:max(100%,44px);
2955
3071
  transform:translate(-50%,-50%)}
2956
3072
  /* The chip rail stacks remove over view in ~53px \u2014 pseudo-targets would
@@ -3047,18 +3163,12 @@ var CSS = (
3047
3163
  border:1px solid rgba(150,165,205,.35);backdrop-filter:blur(6px);cursor:pointer}
3048
3164
  .sl-view3d-nav button:hover,.sl-view3d-nav button:focus-visible{color:#eef1f8;border-color:rgba(190,205,240,.6)}
3049
3165
  .sl-view3d-nav button[aria-pressed="true"]{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
3050
- .sl-view3d-nav button:disabled{opacity:.45;cursor:not-allowed}
3166
+ .sl-view3d-nav button:disabled{opacity:.6;cursor:not-allowed}
3051
3167
  .sl-view3d-nav-toggle{display:none!important}
3052
3168
  .sl-view3d-nav select{min-height:38px;max-width:100%;padding:7px 34px 7px 12px;
3053
3169
  border-radius:999px;font:700 11.5px/1 inherit;color:#eef1f8;background:rgba(12,18,32,.86);
3054
3170
  border:1px solid rgba(150,165,205,.45);backdrop-filter:blur(6px);cursor:pointer}
3055
3171
  .sl-view3d-nav select:focus-visible{outline:2px solid var(--sl-accent);outline-offset:2px}
3056
- .sl-view3d-nav .sl-view3d-locator{display:grid;grid-template-columns:minmax(150px,1.25fr) minmax(110px,.8fr) minmax(105px,.7fr) auto;
3057
- width:min(720px,100%);overflow:visible}
3058
- .sl-view3d-nav.is-seat-focused .sl-view3d-locator{display:none}
3059
- .sl-view3d-locator select{width:100%;min-width:0}
3060
- .sl-picker[data-layout="narrow"] .sl-view3d-nav .sl-view3d-locator{display:flex;width:100%;overflow-x:auto}
3061
- .sl-picker[data-layout="narrow"] .sl-view3d-locator select{flex:0 0 155px}
3062
3172
  /* On phones the library owns the bottom edge for seat/panorama/overview actions.
3063
3173
  Keep venue navigation in a separate top rail so those control families never
3064
3174
  stack over one another. */
@@ -3068,9 +3178,6 @@ var CSS = (
3068
3178
  .sl-picker[data-layout="narrow"] .sl-view3d-nav.is-open{left:12px;top:68px;max-width:calc(100% - 24px);padding:8px;
3069
3179
  border:1px solid rgba(150,165,205,.35);border-radius:14px;background:rgba(8,12,22,.9);backdrop-filter:blur(10px)}
3070
3180
  .sl-picker[data-layout="narrow"] .sl-view3d-nav.is-open>div{display:flex}
3071
- .sl-picker[data-layout="narrow"] .sl-view3d-nav.is-open .sl-view3d-locator{display:grid;grid-template-columns:1fr 1fr;width:100%;overflow:visible}
3072
- .sl-picker[data-layout="narrow"] .sl-view3d-nav.is-open .sl-view3d-locator select{width:100%;min-width:0;flex:auto}
3073
- .sl-picker[data-layout="narrow"] .sl-view3d-nav.is-open .sl-view3d-locator button{width:100%}
3074
3181
  /* Seat-eye is a decision state, not another venue-navigation state. Once the
3075
3182
  buyer arrives, clear the floor/area/locator rails and the module's duplicate
3076
3183
  Overview action. The picker-owned Back button becomes the one predictable
@@ -3104,7 +3211,7 @@ var CSS = (
3104
3211
  .sl-confirm-confidence strong,.sl-confirm-confidence small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3105
3212
  .sl-confirm-confidence strong{font-size:11px}.sl-confirm-confidence small{margin-top:2px;color:var(--sl-muted);font-size:9.5px}
3106
3213
  .sl-confirm-confidence em{display:none;font-style:normal}
3107
- .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)}
3108
3215
  .sl-confirm-confidence:hover,.sl-confirm-confidence:focus-visible{border-color:var(--sl-accent)}
3109
3216
  /* The production 3D decision dock is deliberately denser than the 2D popup:
3110
3217
  the venue remains the main content and the two inspection actions share one
@@ -3186,7 +3293,7 @@ var CSS = (
3186
3293
  .sl-view3d-compare-actions{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;margin-top:12px}
3187
3294
  .sl-view3d-compare-actions button{min-height:44px;border-radius:9px;border:1px solid var(--sl-line);font-size:12px;font-weight:800}
3188
3295
  .sl-view3d-compare-actions .select{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
3189
- .sl-view3d-compare-actions button:disabled{opacity:.5;cursor:not-allowed}
3296
+ .sl-view3d-compare-actions button:disabled{opacity:.62;cursor:not-allowed}
3190
3297
  .sl-view3d-passport-shell{position:absolute;inset:0;z-index:40;display:grid;place-items:center;padding:16px}
3191
3298
  .sl-view3d-passport-scrim{position:absolute;inset:0;background:rgba(3,6,12,.8);backdrop-filter:blur(6px)}
3192
3299
  .sl-view3d-passport{position:relative;width:min(540px,100%);max-height:min(700px,calc(100% - 20px));overflow:auto;
@@ -3227,14 +3334,17 @@ var CSS = (
3227
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)}
3228
3335
  .sl-confirm-3d svg{width:15px;height:15px;stroke:currentColor;stroke-width:1.9;fill:none;stroke-linecap:round;stroke-linejoin:round}
3229
3336
 
3230
- /* multi-floor switcher (flows within the left-rail region) */
3231
- .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)}
3232
3341
  .sl-floors.on{display:flex}
3233
- .sl-floors button{padding:7px 13px;border-radius:999px;font-size:12px;font-weight:700;background:var(--sl-surface);
3234
- border:1px solid var(--sl-line);color:var(--sl-muted);white-space:nowrap;max-width:100%;overflow:hidden;
3235
- text-overflow:ellipsis;transition:color .15s,border-color .15s}
3236
- .sl-floors button:hover{color:var(--sl-text)}
3237
- .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)}
3238
3348
 
3239
3349
  /* tapped-section summary card \u2014 docks INSIDE the top-center anchor region on
3240
3350
  wide (flows below the rung pills, never over them, never floating over the
@@ -3269,8 +3379,14 @@ var CSS = (
3269
3379
  .sl-seccard-mix-dot{width:8px;height:8px;border-radius:50%;flex:none}
3270
3380
  .sl-seccard-mix-price{font-weight:700;color:var(--sl-text)}
3271
3381
  .sl-seccard-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px}
3272
- .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)}
3273
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}
3274
3390
 
3275
3391
  /* view-from-seat button on the confirm popover */
3276
3392
  /* Eager sightline preview inside the confirm card */
@@ -3323,10 +3439,29 @@ var CSS = (
3323
3439
  white-space:nowrap;pointer-events:none;max-width:90%;overflow:hidden;text-overflow:ellipsis}
3324
3440
 
3325
3441
  /* F3 minimap \u2014 venue overview + live viewport rect (flows in the bottom-left region) */
3326
- .sl-minimap{border:1px solid var(--sl-line);border-radius:9px;
3327
- 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}
3328
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}
3329
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}
3330
3465
 
3331
3466
  /* F4 legend reflection: rows + counts for out-of-band categories read muted */
3332
3467
  /* Out-of-band rows are exactly the rows a buyer reads while deciding whether
@@ -3404,17 +3539,88 @@ function formatWhen(startsAt, timezone, locale) {
3404
3539
  }
3405
3540
  return at.toLocaleString(locale, options);
3406
3541
  }
3407
- 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");
3408
3610
  const accent = host?.accent ?? chart?.accent ?? "#f4b740";
3409
- 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));
3410
3615
  return {
3411
3616
  "--sl-accent": accent,
3412
3617
  "--sl-accent-ink": accentInk,
3413
- "--sl-bg": host?.background ?? chart?.background ?? "#0f1522",
3414
- "--sl-surface": host?.surface ?? "#1a2234",
3415
- "--sl-text": host?.text ?? chart?.textColor ?? "#eef1f8",
3416
- "--sl-muted": host?.muted ?? "#8b93a7",
3417
- "--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)"),
3418
3624
  "--sl-font": host?.fontFamily ?? chart?.fontFamily ?? "-apple-system,BlinkMacSystemFont,'Segoe UI',Inter,sans-serif",
3419
3625
  "--sl-radius": `${host?.radius ?? 14}px`
3420
3626
  };
@@ -3496,6 +3702,15 @@ var SeatPicker = class _SeatPicker {
3496
3702
  this.baZone = "";
3497
3703
  /** "★ Best seats" premium quick-pick toggle — biases best-available to premium seats. */
3498
3704
  this.baPremium = false;
3705
+ /**
3706
+ * The buyer re-opened the best-seats form with seats already in the cart
3707
+ * ("I picked one, actually find us three together"). The card normally
3708
+ * yields this space to the ticket chips the moment anything is selected —
3709
+ * which removed the accelerator at exactly the moment a one-handed phone
3710
+ * buyer wants it. Cleared when the cart empties, a hold lands, or the
3711
+ * buyer backs out.
3712
+ */
3713
+ this.baReopen = false;
3499
3714
  this.bestAvailableConfirm = false;
3500
3715
  this.releasingHold = false;
3501
3716
  /** Event sales window is closed (read-only load state / live close). */
@@ -3537,7 +3752,13 @@ var SeatPicker = class _SeatPicker {
3537
3752
  // F3 minimap
3538
3753
  this.miniCanvas = null;
3539
3754
  this.miniBase = null;
3755
+ this.miniWrap = null;
3756
+ this.miniToggle = null;
3757
+ this.miniLabel = null;
3540
3758
  this.miniTf = null;
3759
+ this.miniDrag = null;
3760
+ this.miniSuppressClick = false;
3761
+ this.pickerLayoutSize = { width: 0, height: 0 };
3541
3762
  // F4 price-band filter — active band's category keys (null = all prices)
3542
3763
  this.priceBandKeys = null;
3543
3764
  this.focusedCatKey = null;
@@ -3573,6 +3794,7 @@ var SeatPicker = class _SeatPicker {
3573
3794
  /** Wide-layout ticket panel collapsed (map owns the full width). */
3574
3795
  this.sideCollapsed = false;
3575
3796
  this.sideToggleEl = null;
3797
+ this.pricesBtnEl = null;
3576
3798
  /** True once we've asked the host page to pin us fullscreen (framed, no native). */
3577
3799
  this.framedFs = false;
3578
3800
  /** Last height (px) posted to a host frame; dedupes redundant reports. */
@@ -3637,6 +3859,7 @@ var SeatPicker = class _SeatPicker {
3637
3859
  mapTheme: options.theme?.map ?? null,
3638
3860
  onSelectionChange: () => {
3639
3861
  this.syncTray();
3862
+ this.refreshMinimap();
3640
3863
  if (this.committedSelection().length) this.collapseSectionCard();
3641
3864
  this.syncSelectionTo3d();
3642
3865
  },
@@ -3685,7 +3908,7 @@ var SeatPicker = class _SeatPicker {
3685
3908
  if (this.tableDialog?.physicalSeatIds.includes(seat.id)) this.dismissTableDialog();
3686
3909
  },
3687
3910
  onSelectionLimit: () => {
3688
- this.toast(`You can select up to ${this.maxTickets} tickets for this order.`, "warning");
3911
+ this.toast(t2("picker.maxTicketsForOrder", { count: this.maxTickets }), "warning");
3689
3912
  },
3690
3913
  onViewChange: () => {
3691
3914
  this.reanchorConfirm();
@@ -3696,6 +3919,12 @@ var SeatPicker = class _SeatPicker {
3696
3919
  },
3697
3920
  // Tapped-section glide-in → surface (or clear) the section-summary card.
3698
3921
  onSectionFocus: (summary) => this.showSectionCard(summary),
3922
+ onDeckTap: () => {
3923
+ this.showSectionCard(null);
3924
+ this.syncFloors();
3925
+ this.syncRung();
3926
+ this.refreshMinimap();
3927
+ },
3699
3928
  onFocusSeat: (seat) => this.announceSeat(seat),
3700
3929
  onSeatHover: (d) => this.updateTooltip(d),
3701
3930
  onHint: (m) => {
@@ -3801,8 +4030,8 @@ var SeatPicker = class _SeatPicker {
3801
4030
  return `<span class="sl-cx-mark" role="img" aria-label="${title}" title="${title}">\u25D0</span>`;
3802
4031
  }
3803
4032
  wheelchairProvisionLabel(type) {
3804
- if (type === "no-seat") return "Empty wheelchair space";
3805
- if (type === "seat-present") return "Accessible physical seat";
4033
+ if (type === "no-seat") return this.tf("picker.emptyWheelchairSpace", "Empty wheelchair space");
4034
+ if (type === "seat-present") return this.tf("picker.accessiblePhysicalSeat", "Accessible physical seat");
3806
4035
  return "";
3807
4036
  }
3808
4037
  wheelchairConfirmHtml(type) {
@@ -3883,9 +4112,9 @@ var SeatPicker = class _SeatPicker {
3883
4112
  const hideEventDetails = this.eventDetailsHidden && !active;
3884
4113
  this.root?.setAttribute("data-event-details-hidden", String(hideEventDetails));
3885
4114
  this.els.zfs?.setAttribute("aria-pressed", String(active));
3886
- this.els.zfs?.setAttribute("title", active ? "Exit full screen" : "Full screen");
4115
+ this.els.zfs?.setAttribute("title", active ? this.tf("picker.exitFullScreen", "Exit full screen") : this.tf("picker.fullScreen", "Full screen"));
3887
4116
  const zfsLabel = this.els.zfs?.querySelector(".sl-zfs-lbl");
3888
- if (zfsLabel) zfsLabel.textContent = active ? "Exit full screen" : "Full screen";
4117
+ if (zfsLabel) zfsLabel.textContent = active ? this.tf("picker.exitFullScreen", "Exit full screen") : this.tf("picker.fullScreen", "Full screen");
3889
4118
  this.view3dEl?.querySelector(".sl-view3d-fs")?.setAttribute("aria-pressed", String(active));
3890
4119
  }
3891
4120
  /**
@@ -3913,7 +4142,7 @@ var SeatPicker = class _SeatPicker {
3913
4142
  window.removeEventListener("keydown", this.fsEscHandler);
3914
4143
  this.fsEscHandler = null;
3915
4144
  }
3916
- requestAnimationFrame(() => this.controller.zoomToFit());
4145
+ requestAnimationFrame(() => this.controller.refitCurrentView());
3917
4146
  }
3918
4147
  setFsFallback(on) {
3919
4148
  if (this.fsFallback === on) return;
@@ -3929,7 +4158,7 @@ var SeatPicker = class _SeatPicker {
3929
4158
  window.removeEventListener("keydown", this.fsEscHandler);
3930
4159
  this.fsEscHandler = null;
3931
4160
  }
3932
- requestAnimationFrame(() => this.controller.zoomToFit());
4161
+ requestAnimationFrame(() => this.controller.refitCurrentView());
3933
4162
  }
3934
4163
  /**
3935
4164
  * Close the picker. In modal mode (SeatPicker.open()) this dismisses the
@@ -3950,7 +4179,7 @@ var SeatPicker = class _SeatPicker {
3950
4179
  frame.className = "sl-modal-frame";
3951
4180
  frame.setAttribute("role", "dialog");
3952
4181
  frame.setAttribute("aria-modal", "true");
3953
- frame.setAttribute("aria-label", "Seat selection");
4182
+ frame.setAttribute("aria-label", tfStatic("picker.seatSelection", "Seat selection"));
3954
4183
  frame.tabIndex = -1;
3955
4184
  scrim.appendChild(frame);
3956
4185
  document.body.appendChild(scrim);
@@ -4069,11 +4298,17 @@ var SeatPicker = class _SeatPicker {
4069
4298
  }
4070
4299
  async render() {
4071
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;
4072
4304
  this.rendered = true;
4073
4305
  ensureStyle();
4306
+ const localeStartedAt = performance.now();
4074
4307
  await loadLocale2(this.opts.locale);
4308
+ renderProfile.loadLocale = duration(localeStartedAt);
4075
4309
  if (this.opts.messages) setStringOverrides2(this.opts.messages);
4076
4310
  if (this.checkoutMode === "hosted") this.paymentOptions = this.pubApi.paymentOptions(this.opts.event);
4311
+ const shellStartedAt = performance.now();
4077
4312
  const mount = resolveContainer3(this.opts.container);
4078
4313
  const root = document.createElement("div");
4079
4314
  root.className = "sl-picker";
@@ -4097,7 +4332,7 @@ var SeatPicker = class _SeatPicker {
4097
4332
  this.syncTray();
4098
4333
  }
4099
4334
  });
4100
- 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));
4101
4336
  root.innerHTML = `
4102
4337
  <div class="sl-head">
4103
4338
  <div class="sl-logo" data-ref="logo"></div>
@@ -4110,7 +4345,7 @@ var SeatPicker = class _SeatPicker {
4110
4345
  <svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
4111
4346
  <span data-ref="closedPillText"></span>
4112
4347
  </span>
4113
- <button type="button" class="sl-close" data-ref="close" aria-label="Close">
4348
+ <button type="button" class="sl-close" data-ref="close" aria-label="${this.tf("picker.close", "Close")}">
4114
4349
  <svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
4115
4350
  </button>
4116
4351
  </div>
@@ -4118,17 +4353,17 @@ var SeatPicker = class _SeatPicker {
4118
4353
  <div class="sl-map">
4119
4354
  <div class="sl-map-host" data-ref="map"></div>
4120
4355
  <div class="sl-zoom" data-ref="zoom">
4121
- <button type="button" aria-label="Zoom in" title="Zoom in" data-ref="zin">+</button>
4122
- <button type="button" aria-label="Zoom out" title="Zoom out" data-ref="zout">\u2212</button>
4123
- <button type="button" aria-label="Fit to screen" title="Fit to screen" data-ref="zfit">
4356
+ <button type="button" aria-label="${this.tf("picker.zoomIn", "Zoom in")}" title="${this.tf("picker.zoomIn", "Zoom in")}" data-ref="zin">+</button>
4357
+ <button type="button" aria-label="${this.tf("picker.zoomOut", "Zoom out")}" title="${this.tf("picker.zoomOut", "Zoom out")}" data-ref="zout">\u2212</button>
4358
+ <button type="button" aria-label="${this.tf("picker.fitToScreen", "Fit to screen")}" title="${this.tf("picker.fitToScreen", "Fit to screen")}" data-ref="zfit">
4124
4359
  <svg viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
4125
4360
  </button>
4126
- <button type="button" aria-label="Full screen" title="Full screen" aria-pressed="false" data-ref="zfs">
4361
+ <button type="button" aria-label="${this.tf("picker.fullScreen", "Full screen")}" title="${this.tf("picker.fullScreen", "Full screen")}" aria-pressed="false" data-ref="zfs">
4127
4362
  <svg viewBox="0 0 24 24"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
4128
- <span class="sl-zfs-lbl" aria-hidden="true">Full screen</span>
4363
+ <span class="sl-zfs-lbl" aria-hidden="true">${this.tf("picker.fullScreen", "Full screen")}</span>
4129
4364
  </button>
4130
4365
  </div>
4131
- <div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
4366
+ <div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>${this.tf("picker.loadingSeatMap", "Loading seat map\u2026")}</div>
4132
4367
  <div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
4133
4368
  </div>
4134
4369
  <div class="sl-side" data-ref="side">
@@ -4136,24 +4371,25 @@ var SeatPicker = class _SeatPicker {
4136
4371
  <div class="sl-sheet-grab"></div>
4137
4372
  <div class="sl-sheet-bar">
4138
4373
  <div class="sl-sheet-peek" data-ref="peek"></div>
4139
- <button type="button" class="sl-sheet-toggle" data-ref="sheetToggle" aria-label="Open ticket panel" aria-expanded="false">
4374
+ <button type="button" class="sl-sheet-toggle" data-ref="sheetToggle" aria-label="${this.tf("picker.openTicketPanel", "Open ticket panel")}" aria-expanded="false">
4140
4375
  <svg viewBox="0 0 24 24"><path d="M6 15l6-6 6 6"/></svg>
4141
4376
  </button>
4142
4377
  </div>
4143
4378
  </div>
4144
- <div class="sl-sec sl-filtersec" data-ref="filtersSec">Filters</div>
4379
+ <div class="sl-sec sl-filtersec" data-ref="filtersSec">${this.tf("picker.filters", "Filters")}</div>
4145
4380
  <div class="sl-filters" data-ref="filters"></div>
4146
4381
  <div class="sl-offer" data-ref="offer" role="status" aria-live="polite"></div>
4147
- <div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>Ticket prices</span></div>
4382
+ <div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>${this.tf("picker.ticketPrices", "Ticket prices")}</span></div>
4383
+ <div class="sl-prices-hint" data-ref="pricesHint"></div>
4148
4384
  <div class="sl-prices" data-ref="prices"></div>
4149
- <div class="sl-live" data-ref="live" role="status" aria-live="polite"><span class="dot" aria-hidden="true"></span><span data-ref="liveText">Live availability \u2014 seats update in real time</span></div>
4150
- <div class="sl-sec sl-seats-sec"><span>Your seats</span><span class="sl-seat-summary" data-ref="seatSummary"></span></div>
4385
+ <div class="sl-live" data-ref="live" role="status" aria-live="polite"><span class="dot" aria-hidden="true"></span><span data-ref="liveText">${this.tf("picker.liveAvailability", "Live availability \u2014 seats update in real time")}</span></div>
4386
+ <div class="sl-sec sl-seats-sec"><span>${this.tf("picker.yourSeats", "Your seats")}</span><span class="sl-seat-summary" data-ref="seatSummary"></span></div>
4151
4387
  <div class="sl-tray" data-ref="tray"></div>
4152
4388
  <div class="sl-foot" data-ref="foot">
4153
4389
  <div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
4154
4390
  <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
4155
- <span><b data-ref="holdTitle">Seats secured</b><span class="sl-hold-copy" data-ref="holdCopy">You won\u2019t be charged yet.</span></span>
4156
- <button type="button" class="sl-hold-change" data-ref="holdChange" aria-label="Release held tickets and choose different seats">Change</button>
4391
+ <span><b data-ref="holdTitle">${this.tf("picker.seatsSecured", "Seats secured")}</b><span class="sl-hold-copy" data-ref="holdCopy">${this.tf("picker.notChargedYet", "You won\u2019t be charged yet.")}</span></span>
4392
+ <button type="button" class="sl-hold-change" data-ref="holdChange" aria-label="${this.tf("picker.releaseHeldTickets", "Release held tickets and choose different seats")}">${this.tf("picker.change", "Change")}</button>
4157
4393
  </div>
4158
4394
  <div class="sl-total"><span data-ref="count"></span><b data-ref="total"></b></div>
4159
4395
  <button type="button" class="sl-cta" data-ref="cta" disabled></button>
@@ -4175,14 +4411,20 @@ var SeatPicker = class _SeatPicker {
4175
4411
  const applyLayout = () => {
4176
4412
  const w = root.clientWidth;
4177
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 };
4178
4417
  this.reportFramedHeight();
4179
4418
  const next = w < 640 ? "narrow" : "wide";
4180
- const density = root.clientHeight < 560 ? "compact" : "comfortable";
4181
- if (root.dataset.layout === next && root.dataset.density === density) return;
4182
- root.dataset.layout = next;
4183
- root.dataset.density = density;
4184
- if (next === "narrow" && !root.dataset.sheet) root.dataset.sheet = "peek";
4185
- 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();
4186
4428
  };
4187
4429
  this.ro = new ResizeObserver(applyLayout);
4188
4430
  this.ro.observe(root);
@@ -4195,7 +4437,7 @@ var SeatPicker = class _SeatPicker {
4195
4437
  this.fsChangeHandler = () => {
4196
4438
  if (!document.fullscreenElement) this.setFsFallback(false);
4197
4439
  this.syncFullscreenButtons();
4198
- requestAnimationFrame(() => this.controller.zoomToFit());
4440
+ requestAnimationFrame(() => this.controller.refitCurrentView());
4199
4441
  };
4200
4442
  document.addEventListener("fullscreenchange", this.fsChangeHandler);
4201
4443
  const head = this.els.sheetHead;
@@ -4204,7 +4446,7 @@ var SeatPicker = class _SeatPicker {
4204
4446
  const setSheet = (open) => {
4205
4447
  root.dataset.sheet = open ? "open" : "peek";
4206
4448
  toggle?.setAttribute("aria-expanded", String(open));
4207
- toggle?.setAttribute("aria-label", open ? "Collapse ticket panel" : "Open ticket panel");
4449
+ toggle?.setAttribute("aria-label", open ? this.tf("picker.collapseTicketPanel", "Collapse ticket panel") : this.tf("picker.openTicketPanel", "Open ticket panel"));
4208
4450
  };
4209
4451
  setSheet(root.dataset.sheet === "open");
4210
4452
  toggle?.addEventListener("click", (e) => {
@@ -4250,6 +4492,39 @@ var SeatPicker = class _SeatPicker {
4250
4492
  head.releasePointerCapture?.(e.pointerId);
4251
4493
  });
4252
4494
  }
4495
+ const togglePrices = () => {
4496
+ if (this.root?.dataset.layout !== "narrow") return;
4497
+ const open = this.root.getAttribute("data-prices-open") === "true";
4498
+ this.root.setAttribute("data-prices-open", String(!open));
4499
+ this.els.pricesSec?.setAttribute("aria-expanded", String(!open));
4500
+ this.pricesBtnEl?.setAttribute("aria-pressed", String(!open));
4501
+ if (!open && this.root.dataset.sheet !== "open") {
4502
+ this.root.dataset.sheet = "open";
4503
+ const sheetToggle = this.els.sheetToggle;
4504
+ sheetToggle?.setAttribute("aria-expanded", "true");
4505
+ sheetToggle?.setAttribute("aria-label", "Collapse ticket panel");
4506
+ }
4507
+ };
4508
+ this.els.pricesSec?.addEventListener("click", (e) => {
4509
+ if (e.target.closest("select,button")) return;
4510
+ togglePrices();
4511
+ });
4512
+ this.els.pricesSec?.addEventListener("keydown", (e) => {
4513
+ if (e.key !== "Enter" && e.key !== " ") return;
4514
+ if (e.target.closest("select,button")) return;
4515
+ e.preventDefault();
4516
+ togglePrices();
4517
+ });
4518
+ const pricesBtn = document.createElement("button");
4519
+ pricesBtn.type = "button";
4520
+ pricesBtn.className = "sl-prices-btn";
4521
+ pricesBtn.setAttribute("aria-label", this.tf("picker.ticketPrices", "Ticket prices"));
4522
+ pricesBtn.setAttribute("title", this.tf("picker.ticketPrices", "Ticket prices"));
4523
+ pricesBtn.setAttribute("aria-pressed", "false");
4524
+ pricesBtn.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20.6 13.4 13.4 20.6a2 2 0 0 1-2.8 0L3 13V5a2 2 0 0 1 2-2h8l7.6 7.6a2 2 0 0 1 0 2.8z"/><circle cx="7.5" cy="7.5" r="1.3"/></svg>';
4525
+ pricesBtn.addEventListener("click", () => togglePrices());
4526
+ this.els.headInfo?.parentElement?.insertBefore(pricesBtn, this.els.hold ?? null);
4527
+ this.pricesBtnEl = pricesBtn;
4253
4528
  this.tipEl = document.createElement("div");
4254
4529
  this.tipEl.setAttribute("role", "tooltip");
4255
4530
  this.tipEl.className = "sl-tip";
@@ -4264,10 +4539,13 @@ var SeatPicker = class _SeatPicker {
4264
4539
  const canvasHost = document.createElement("div");
4265
4540
  canvasHost.style.cssText = "position:absolute;inset:0";
4266
4541
  this.mapHost.appendChild(canvasHost);
4542
+ renderProfile.mountBuyerShell = duration(shellStartedAt);
4543
+ const chartStartedAt = performance.now();
4267
4544
  const info = await this.controller.render(canvasHost);
4545
+ renderProfile.loadChartAndStatuses = duration(chartStartedAt);
4268
4546
  if (this.destroyed) return this;
4269
4547
  if (!info) {
4270
- this.els.boot.innerHTML = '<div class="sl-boot-title">The seat map didn\u2019t load</div><div>Check your connection and try again.</div><button type="button" class="sl-boot-retry">Try again</button>';
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>`;
4271
4549
  this.els.boot.querySelector("button").addEventListener("click", () => {
4272
4550
  const container = this.opts.container;
4273
4551
  const opts = this.opts;
@@ -4276,7 +4554,9 @@ var SeatPicker = class _SeatPicker {
4276
4554
  });
4277
4555
  return this;
4278
4556
  }
4557
+ Object.assign(renderProfile, info.performanceProfile);
4279
4558
  this.els.boot.remove();
4559
+ const controlsStartedAt = performance.now();
4280
4560
  this.startRealtime();
4281
4561
  this.salesClosed = !!info.salesClosed || !!this.opts.readOnly;
4282
4562
  root.dataset.eventMode = info.mode === "test" ? "test" : "live";
@@ -4292,7 +4572,7 @@ var SeatPicker = class _SeatPicker {
4292
4572
  this.regions["top-left"].appendChild(badge);
4293
4573
  }
4294
4574
  const chartTheme = this.controller.doc?.theme;
4295
- 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));
4296
4576
  this.currency = info.currency ?? this.opts.currency ?? "USD";
4297
4577
  this.eventTimezone = info.timezone ?? null;
4298
4578
  const logoUrl = this.opts.theme?.logoUrl ?? chartTheme?.logoUrl ?? info.posterUrl ?? null;
@@ -4302,6 +4582,12 @@ var SeatPicker = class _SeatPicker {
4302
4582
  const when = info.startsAt ? formatWhen(info.startsAt, info.timezone ?? null, this.opts.locale) : "";
4303
4583
  this.eventWhenText = when || null;
4304
4584
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
4585
+ if (this.els.pricesHint) {
4586
+ this.els.pricesHint.textContent = this.tf(
4587
+ "picker.priceHint",
4588
+ "Colours on the map are ticket types \u2014 tap one below to show just those seats."
4589
+ );
4590
+ }
4305
4591
  this.buildBadge(chartTheme);
4306
4592
  const present = /* @__PURE__ */ new Set();
4307
4593
  let hasLimitedView = false;
@@ -4328,7 +4614,7 @@ var SeatPicker = class _SeatPicker {
4328
4614
  const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-a11y="1" data-f="${key}">${label}</button>`;
4329
4615
  chips.insertAdjacentHTML(
4330
4616
  "beforeend",
4331
- mk("all", "All seats") + ACCESSIBILITY_TYPES.filter(({ key }) => present.has(key)).map(({ key, short, icon }) => mk(key, `${icon} ${short}`)).join("")
4617
+ mk("all", this.tf("picker.allSeats", "All seats")) + ACCESSIBILITY_TYPES.filter(({ key }) => present.has(key)).map(({ key, short, icon }) => mk(key, `${icon} ${short}`)).join("")
4332
4618
  );
4333
4619
  const active = /* @__PURE__ */ new Set();
4334
4620
  const syncChips = () => {
@@ -4374,7 +4660,7 @@ var SeatPicker = class _SeatPicker {
4374
4660
  cb.type = "button";
4375
4661
  cb.className = "sl-cbbtn";
4376
4662
  this.cbEl = cb;
4377
- cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
4663
+ cb.setAttribute("aria-label", this.tf("picker.toggleColorblindColors", "Toggle colorblind-friendly colors"));
4378
4664
  cb.setAttribute("aria-pressed", String(this.cbSafe));
4379
4665
  cb.innerHTML = '<svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
4380
4666
  this.els.zfit.parentElement.appendChild(cb);
@@ -4394,12 +4680,34 @@ var SeatPicker = class _SeatPicker {
4394
4680
  this.buildPanelToggle();
4395
4681
  if (this.opts.panelCollapsed) this.setPanelCollapsed(true);
4396
4682
  this.dockLayoutChrome();
4683
+ renderProfile.buildBuyerControls = duration(controlsStartedAt);
4684
+ const holdStartedAt = performance.now();
4397
4685
  await this.restoreRememberedHold();
4686
+ renderProfile.restoreHold = duration(holdStartedAt);
4398
4687
  if (this.destroyed) return this;
4399
4688
  if (this.salesClosed) this.applySalesClosed();
4400
4689
  this.syncPrices();
4401
4690
  this.syncTray();
4402
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
+ });
4403
4711
  return this;
4404
4712
  }
4405
4713
  /**
@@ -4460,8 +4768,9 @@ var SeatPicker = class _SeatPicker {
4460
4768
  if (!btn) return;
4461
4769
  const collapsed = this.sideCollapsed;
4462
4770
  btn.setAttribute("aria-expanded", String(!collapsed));
4463
- btn.setAttribute("aria-label", collapsed ? "Show the ticket panel" : "Hide the ticket panel");
4464
- btn.innerHTML = collapsed ? '<svg viewBox="0 0 24 24"><path d="M15 6l-6 6 6 6"/></svg><span>Tickets</span>' : '<svg viewBox="0 0 24 24"><path d="M9 6l6 6-6 6"/></svg><span>Hide panel</span>';
4771
+ btn.setAttribute("aria-label", collapsed ? this.tf("picker.showTicketPanel", "Show the ticket panel") : this.tf("picker.hideTicketPanel", "Hide the ticket panel"));
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>`;
4465
4774
  }
4466
4775
  /**
4467
4776
  * Collapse or reopen the wide-layout ticket panel in place (host-drivable —
@@ -4474,7 +4783,7 @@ var SeatPicker = class _SeatPicker {
4474
4783
  if (this.destroyed) return;
4475
4784
  this.sideCollapsed = collapsed;
4476
4785
  this.applyPanelCollapsed();
4477
- this.scheduleMotion(() => this.controller.zoomToFit(), 340);
4786
+ this.scheduleMotion(() => this.controller.refitCurrentView(), 340);
4478
4787
  }
4479
4788
  /** Idempotent DOM apply, also run on every layout flip (inert must lift on narrow). */
4480
4789
  applyPanelCollapsed() {
@@ -4487,13 +4796,24 @@ var SeatPicker = class _SeatPicker {
4487
4796
  const narrow = this.root?.dataset.layout === "narrow";
4488
4797
  this.applyPanelCollapsed();
4489
4798
  const zfs = this.els.zfs;
4490
- if (zfs) {
4491
- if (narrow && this.regions["top-right"]) {
4492
- zfs.classList.add("sl-fs-pill");
4493
- this.regions["top-right"].appendChild(zfs);
4494
- } else if (!narrow && this.els.zoom && zfs.parentElement !== this.els.zoom) {
4495
- zfs.classList.remove("sl-fs-pill");
4496
- this.els.zoom.appendChild(zfs);
4799
+ const head = this.els.headInfo?.parentElement;
4800
+ if (zfs && head && zfs.parentElement !== head) {
4801
+ zfs.classList.add("sl-fs-pill");
4802
+ head.insertBefore(zfs, this.els.hold ?? null);
4803
+ }
4804
+ const pricesSec = this.els.pricesSec;
4805
+ if (pricesSec) {
4806
+ if (narrow) {
4807
+ pricesSec.setAttribute("role", "button");
4808
+ pricesSec.tabIndex = 0;
4809
+ pricesSec.setAttribute(
4810
+ "aria-expanded",
4811
+ String(this.root?.getAttribute("data-prices-open") === "true")
4812
+ );
4813
+ } else {
4814
+ pricesSec.removeAttribute("role");
4815
+ pricesSec.removeAttribute("aria-expanded");
4816
+ pricesSec.tabIndex = -1;
4497
4817
  }
4498
4818
  }
4499
4819
  const filters = this.els.filters;
@@ -4520,7 +4840,7 @@ var SeatPicker = class _SeatPicker {
4520
4840
  this.extendEl = el;
4521
4841
  this.els.extendTxt = el.querySelector('[data-ref="extendTxt"]');
4522
4842
  this.els.extendBtn = el.querySelector('[data-ref="extendBtn"]');
4523
- this.els.extendBtn.textContent = "Add time";
4843
+ this.els.extendBtn.textContent = this.tf("picker.addTime", "Add time");
4524
4844
  this.els.extendBtn.addEventListener("click", () => void this.handleExtend());
4525
4845
  }
4526
4846
  /** Success overlay + onBooked fire when the held seats settle to booked. */
@@ -4529,7 +4849,7 @@ var SeatPicker = class _SeatPicker {
4529
4849
  el.className = "sl-booked";
4530
4850
  el.setAttribute("role", "status");
4531
4851
  el.setAttribute("aria-live", "polite");
4532
- el.innerHTML = `<div class="sl-booked-badge"><svg viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></div><div class="sl-booked-title">You're all set</div><div class="sl-booked-sub" data-ref="bookedSub"></div>`;
4852
+ el.innerHTML = `<div class="sl-booked-badge"><svg viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></div><div class="sl-booked-title">${this.tf("picker.allSetTitle", "You're all set")}</div><div class="sl-booked-sub" data-ref="bookedSub"></div>`;
4533
4853
  this.root.appendChild(el);
4534
4854
  this.bookedEl = el;
4535
4855
  this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
@@ -4540,8 +4860,7 @@ var SeatPicker = class _SeatPicker {
4540
4860
  * host `messages` override (which makes `t()` return the override, not the key).
4541
4861
  */
4542
4862
  tf(key, fallback) {
4543
- const v = t2(key);
4544
- return v === key ? fallback : v;
4863
+ return tfStatic(key, fallback);
4545
4864
  }
4546
4865
  /** Sold-out overlay — an informational state with no unavailable action. */
4547
4866
  buildSoldoutOverlay() {
@@ -4726,7 +5045,7 @@ var SeatPicker = class _SeatPicker {
4726
5045
  }
4727
5046
  canAddTicket() {
4728
5047
  if (this.totalTicketCount() < this.maxTickets) return true;
4729
- this.toast(`You can select up to ${this.maxTickets} tickets for this order.`, "warning");
5048
+ this.toast(t2("picker.maxTicketsForOrder", { count: this.maxTickets }), "warning");
4730
5049
  return false;
4731
5050
  }
4732
5051
  pendingGATotal(gaAreas) {
@@ -4749,21 +5068,21 @@ var SeatPicker = class _SeatPicker {
4749
5068
  }
4750
5069
  if (this.confirmSeat || this.tableDialog && !this.tableDialogHeld) {
4751
5070
  cta.disabled = true;
4752
- cta.textContent = this.tableDialog ? "Confirm your table" : "Confirm or cancel this seat";
5071
+ cta.textContent = this.tableDialog ? this.tf("picker.confirmYourTable", "Confirm your table") : this.tf("picker.confirmOrCancelSeat", "Confirm or cancel this seat");
4753
5072
  return;
4754
5073
  }
4755
5074
  if (this.ctaPhase === "holding") {
4756
5075
  cta.disabled = true;
4757
- cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Securing seats\u2026';
5076
+ cta.innerHTML = `<span class="sl-cta-spin" aria-hidden="true"></span>${this.tf("picker.securingSeats", "Securing your seats\u2026")}`;
4758
5077
  return;
4759
5078
  }
4760
5079
  if (this.ctaPhase === "checkout") {
4761
5080
  cta.disabled = true;
4762
- cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Opening secure checkout\u2026';
5081
+ cta.innerHTML = `<span class="sl-cta-spin" aria-hidden="true"></span>${this.tf("picker.openingCheckout", "Opening secure checkout\u2026")}`;
4763
5082
  return;
4764
5083
  }
4765
5084
  cta.disabled = count === 0;
4766
- cta.textContent = this.hold ? pending ? `Secure ${pending} more & checkout` : "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
5085
+ cta.textContent = this.hold ? pending ? t2("picker.secureMoreAndCheckout", { count: pending }) : this.tf("picker.continueToCheckout", "Continue to checkout") : count ? this.tf("picker.holdSeatsAndCheckout", "Hold seats & checkout") : this.tf("picker.selectSeats", "Select seats");
4767
5086
  }
4768
5087
  setCtaPhase(phase) {
4769
5088
  this.ctaPhase = phase;
@@ -4824,7 +5143,7 @@ var SeatPicker = class _SeatPicker {
4824
5143
  this.syncTray();
4825
5144
  this.emitHoldChange();
4826
5145
  this.opts.onHoldRestored?.(restored, restored.seats ?? [], this.buildHandoff(restored));
4827
- if (automatic) this.toast("Your held tickets have been restored.", "success");
5146
+ if (automatic) this.toast(this.tf("picker.heldTicketsRestored", "Your held tickets have been restored."), "success");
4828
5147
  return restored;
4829
5148
  } catch (error) {
4830
5149
  const status = error?.status;
@@ -4859,10 +5178,77 @@ var SeatPicker = class _SeatPicker {
4859
5178
  buildMinimap() {
4860
5179
  const vp = this.controller.getViewport();
4861
5180
  if (!vp || !this.els.map) return;
4862
- const b = vp.bounds;
4863
- if (!(b.width > 0 && b.height > 0)) return;
4864
- const MAXW = 158;
4865
- 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;
4866
5252
  const PAD = 6;
4867
5253
  const aspect = b.width / Math.max(1, b.height);
4868
5254
  let w = MAXW;
@@ -4874,32 +5260,40 @@ var SeatPicker = class _SeatPicker {
4874
5260
  w = Math.max(64, w);
4875
5261
  h = Math.max(48, h);
4876
5262
  const dpr = Math.min(2, window.devicePixelRatio || 1);
4877
- const wrap = document.createElement("div");
4878
- wrap.className = "sl-minimap";
4879
- wrap.setAttribute("aria-hidden", "true");
4880
- const canvas = document.createElement("canvas");
4881
5263
  canvas.width = Math.round(w * dpr);
4882
5264
  canvas.height = Math.round(h * dpr);
4883
5265
  canvas.style.width = `${w}px`;
4884
5266
  canvas.style.height = `${h}px`;
4885
- wrap.appendChild(canvas);
4886
- (this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
4887
- this.miniCanvas = canvas;
4888
- const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
4889
- const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
4890
- const offY = (h * dpr - b.height * scale) / 2 - b.y * scale;
4891
- this.miniTf = { scale, offX, offY, dpr };
4892
- const base = document.createElement("canvas");
4893
5267
  base.width = canvas.width;
4894
5268
  base.height = canvas.height;
4895
- this.miniBase = base;
4896
- wrap.addEventListener("click", (e) => this.minimapJump(e));
4897
- this.drawMinimapStatic();
4898
- 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
+ );
4899
5291
  }
4900
5292
  /** Repaint the static overview + rect (floor switch, live open/close). */
4901
5293
  refreshMinimap() {
4902
5294
  if (!this.miniBase) return;
5295
+ this.layoutMinimap();
5296
+ this.syncMinimapLabel();
4903
5297
  this.drawMinimapStatic();
4904
5298
  this.drawMinimapRect();
4905
5299
  }
@@ -4907,8 +5301,7 @@ var SeatPicker = class _SeatPicker {
4907
5301
  drawMinimapStatic() {
4908
5302
  const base = this.miniBase;
4909
5303
  const tf = this.miniTf;
4910
- const doc = this.controller.doc;
4911
- if (!base || !tf || !doc) return;
5304
+ if (!base || !tf) return;
4912
5305
  const ctx = base.getContext("2d");
4913
5306
  if (!ctx) return;
4914
5307
  ctx.clearRect(0, 0, base.width, base.height);
@@ -4917,34 +5310,42 @@ var SeatPicker = class _SeatPicker {
4917
5310
  const line = this.cssVar("--sl-line") || "rgba(139,147,167,.5)";
4918
5311
  const muted = this.cssVar("--sl-muted") || "#8b93a7";
4919
5312
  const accent = this.cssVar("--sl-accent") || "#6e7bff";
4920
- const zoneColor = new Map((doc.zones ?? []).map((z) => [z.id, z.color]));
4921
- let drewSection = false;
4922
- for (const o of this.activeFloorObjects()) {
4923
- if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
4924
- drewSection = true;
4925
- const closed = this.controller.isSectionClosed(o.id);
4926
- 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;
4927
5317
  ctx.beginPath();
4928
- 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)));
4929
5319
  ctx.closePath();
4930
- ctx.globalAlpha = closed ? 0.26 : 0.42;
4931
- 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;
4932
5322
  ctx.fill();
4933
- ctx.globalAlpha = 0.85;
5323
+ ctx.globalAlpha = 0.82;
4934
5324
  ctx.lineWidth = Math.max(1, tf.dpr);
4935
5325
  ctx.strokeStyle = line;
4936
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
+ }
4937
5337
  }
4938
5338
  ctx.globalAlpha = 1;
4939
- if (!drewSection) {
5339
+ if (!snapshot.sections.length) {
4940
5340
  const r = Math.max(1, tf.dpr);
4941
- for (const seat of expandChart(doc)) {
4942
- const cat = doc.categories.find((c) => c.key === seat.categoryKey);
4943
- 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;
4944
5344
  ctx.beginPath();
4945
5345
  ctx.arc(fx(seat.x), fy(seat.y), r, 0, Math.PI * 2);
4946
5346
  ctx.fill();
4947
5347
  }
5348
+ ctx.globalAlpha = 1;
4948
5349
  }
4949
5350
  }
4950
5351
  /** Blit the base overview, then stroke the current viewport rectangle on top. */
@@ -4959,41 +5360,106 @@ var SeatPicker = class _SeatPicker {
4959
5360
  ctx.drawImage(base, 0, 0);
4960
5361
  const vp = this.controller.getViewport();
4961
5362
  if (!vp) return;
4962
- const v = vp.visible;
4963
- const x = v.x * tf.scale + tf.offX;
4964
- const y = v.y * tf.scale + tf.offY;
4965
- const w = v.width * tf.scale;
4966
- const h = v.height * tf.scale;
4967
- 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";
4968
5366
  ctx.save();
4969
- ctx.globalAlpha = 0.14;
4970
- ctx.fillStyle = accent;
4971
- 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);
4972
5375
  ctx.globalAlpha = 1;
4973
- ctx.lineWidth = Math.max(1.5, tf.dpr * 1.5);
4974
- ctx.strokeStyle = accent;
4975
- 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
+ }
4976
5383
  ctx.restore();
4977
5384
  }
4978
- /** Minimap click → focus the section under the point (or overview on a miss). */
4979
- minimapJump(e) {
5385
+ minimapWorldPoint(event) {
4980
5386
  const canvas = this.miniCanvas;
4981
5387
  const tf = this.miniTf;
4982
- if (!canvas || !tf) return;
4983
- const r = canvas.getBoundingClientRect();
4984
- const px = (e.clientX - r.left) * (canvas.width / r.width);
4985
- const py = (e.clientY - r.top) * (canvas.height / r.height);
4986
- const wx = (px - tf.offX) / tf.scale;
4987
- const wy = (py - tf.offY) / tf.scale;
4988
- for (const o of this.activeFloorObjects()) {
4989
- if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
4990
- if (this.controller.isSectionClosed(o.id)) continue;
4991
- if (pointInPolygon(wx, wy, o.outline)) {
4992
- 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);
4993
5458
  return;
4994
5459
  }
4995
5460
  }
4996
- this.controller.overview();
5461
+ this.controller.clearSectionFocus();
5462
+ this.controller.panToWorld(point);
4997
5463
  }
4998
5464
  // ---- F4 price-band filter -------------------------------------------------
4999
5465
  /** Effective display price of a category: host pricing override → first tier → base. */
@@ -5002,32 +5468,39 @@ var SeatPicker = class _SeatPicker {
5002
5468
  if (chart === void 0 || !c.key) return chart;
5003
5469
  return this.paidPrice(c.key, c.tiers?.[0]?.id ?? null, chart);
5004
5470
  }
5005
- /** 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. */
5006
5478
  priceBands() {
5007
5479
  const doc = this.controller.doc;
5008
5480
  if (!doc) return [];
5009
- 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);
5010
5482
  if (!priced.length) return [];
5011
- 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);
5012
5484
  if (distinct.length <= 5) {
5013
- return distinct.map((price) => ({
5014
- id: `p${price}`,
5015
- label: this.money(price),
5016
- keys: priced.filter((p) => p.price === price).map((p) => p.key),
5017
- min: price,
5018
- 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
5019
5491
  }));
5020
5492
  }
5021
- 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);
5022
5495
  const bands = [];
5023
- for (let i = 0; i < distinct.length; i += chunk) {
5024
- const slice = distinct.slice(i, i + chunk);
5025
- const lo = slice[0];
5026
- 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));
5027
5500
  bands.push({
5028
5501
  id: `b${i}`,
5029
5502
  label: lo === hi ? this.money(lo) : `${this.money(lo)}\u2013${this.money(hi)}`,
5030
- keys: priced.filter((p) => p.price >= lo && p.price <= hi).map((p) => p.key),
5503
+ keys: slice.map((item) => item.key),
5031
5504
  min: lo,
5032
5505
  max: hi
5033
5506
  });
@@ -5042,8 +5515,8 @@ var SeatPicker = class _SeatPicker {
5042
5515
  if (bands.length < 2) return;
5043
5516
  const select = document.createElement("select");
5044
5517
  select.className = "sl-price-select";
5045
- select.setAttribute("aria-label", "Filter and focus seats by price");
5046
- select.innerHTML = `<option value="all">All prices</option>` + bands.map((band) => `<option value="${band.id}">${band.label}</option>`).join("");
5518
+ select.setAttribute("aria-label", this.tf("picker.filterAndFocusByPrice", "Filter and focus seats by price"));
5519
+ select.innerHTML = `<option value="all">${this.tf("picker.allPrices", "All prices")}</option>` + bands.map((band) => `<option value="${band.id}">${band.label}</option>`).join("");
5047
5520
  this.els.pricesSec.appendChild(select);
5048
5521
  select.addEventListener("change", () => {
5049
5522
  const band = bands.find((candidate) => candidate.id === select.value);
@@ -5070,8 +5543,8 @@ var SeatPicker = class _SeatPicker {
5070
5543
  const projection = document.createElement("div");
5071
5544
  projection.className = "sl-projection";
5072
5545
  projection.setAttribute("role", "group");
5073
- projection.setAttribute("aria-label", "Venue view");
5074
- projection.innerHTML = '<button type="button" data-view="map" aria-pressed="true" title="Flat 2D map">Map</button><button type="button" data-view="venue3d" aria-pressed="false" title="Interactive 3D venue view">3D</button>';
5546
+ projection.setAttribute("aria-label", this.tf("picker.venueView", "Venue view"));
5547
+ projection.innerHTML = `<button type="button" data-view="map" aria-pressed="true" title="${this.tf("picker.flat2dMap", "Flat 2D map")}">${this.tf("picker.map", "Map")}</button><button type="button" data-view="venue3d" aria-pressed="false" title="${this.tf("picker.interactive3dVenueView", "Interactive 3D venue view")}">3D</button>`;
5075
5548
  projection.querySelectorAll("button").forEach((button) => {
5076
5549
  button.addEventListener("click", () => {
5077
5550
  this.setBuyerView(button.dataset.view);
@@ -5082,7 +5555,13 @@ var SeatPicker = class _SeatPicker {
5082
5555
  this.syncProjection();
5083
5556
  }
5084
5557
  if (hasSections) {
5085
- 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"];
5086
5565
  const pills = document.createElement("div");
5087
5566
  pills.className = "sl-rungs on";
5088
5567
  pills.setAttribute("role", "group");
@@ -5115,18 +5594,38 @@ var SeatPicker = class _SeatPicker {
5115
5594
  const floors = this.controller.getFloors();
5116
5595
  const rail = document.createElement("div");
5117
5596
  rail.className = "sl-floors on";
5118
- rail.setAttribute("role", "group");
5119
- rail.setAttribute("aria-label", t2("picker.floor"));
5120
- rail.innerHTML = floors.map((f) => `<button type="button" data-floor="${f.id}">${f.name}</button>`).join("");
5121
- rail.querySelectorAll("button").forEach((btn) => {
5122
- btn.addEventListener("click", () => {
5123
- this.controller.setFloor(btn.dataset.floor);
5124
- this.showSectionCard(null);
5125
- this.syncFloors();
5126
- this.syncRung();
5127
- this.refreshMinimap();
5128
- });
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();
5129
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);
5130
5629
  this.regions["left-rail"].appendChild(rail);
5131
5630
  this.floorsEl = rail;
5132
5631
  this.syncFloors();
@@ -5172,19 +5671,59 @@ var SeatPicker = class _SeatPicker {
5172
5671
  syncFloors() {
5173
5672
  if (!this.floorsEl) return;
5174
5673
  const active = this.controller.getActiveFloorId();
5175
- this.floorsEl.querySelectorAll("button").forEach((btn) => {
5176
- btn.classList.toggle("on", btn.dataset.floor === active);
5177
- });
5674
+ const overview = this.controller.isFloorOverview();
5675
+ const select = this.floorsEl.querySelector("select");
5676
+ if (select) select.value = overview ? "" : active;
5178
5677
  }
5179
5678
  /** Show (or clear, on null) the tapped-section summary card. */
5180
5679
  showSectionCard(summary) {
5181
5680
  this.lastSection = summary;
5182
5681
  this.secCardEl?.remove();
5183
5682
  this.secCardEl = null;
5184
- if (!summary) return;
5683
+ if (!summary) {
5684
+ this.refreshMinimap();
5685
+ return;
5686
+ }
5185
5687
  this.secCardCollapsed = this.controller.getRung() === "seats";
5186
5688
  this.secCardShownAt = Date.now();
5187
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
+ });
5188
5727
  }
5189
5728
  /**
5190
5729
  * Render the section card in the form the layout + state want: expanded card
@@ -5194,26 +5733,36 @@ var SeatPicker = class _SeatPicker {
5194
5733
  renderSectionCard(summary) {
5195
5734
  if (!this.els.map) return;
5196
5735
  this.secCardEl?.remove();
5197
- const paid = summary.categories.length ? summary.categories.map((c) => this.paidPrice(c.key, null, c.price)) : [summary.priceMin, summary.priceMax];
5198
- const paidMin = Math.min(...paid);
5199
- const paidMax = Math.max(...paid);
5736
+ if (this.root?.dataset.layout === "narrow" && (this.hold || this.committedSelection().length || this.pendingGACount())) {
5737
+ this.lastSection = null;
5738
+ return;
5739
+ }
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));
5200
5746
  const priceLabel = paidMin === paidMax ? this.money(paidMin) : `${this.money(paidMin)}\u2013${this.money(paidMax)}`;
5201
5747
  const leftLabel = tCount("picker.seatsLeftInSection", summary.seatsLeft);
5202
5748
  const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${t2("picker.closeSectionSummary")}">\u2715</button>`;
5749
+ const sectionNav = this.sectionNavigationHtml(summary);
5203
5750
  const card = document.createElement("div");
5204
5751
  const narrow = this.root?.dataset.layout === "narrow";
5205
5752
  if (narrow) {
5206
5753
  card.className = "sl-seccard strip on";
5207
5754
  card.setAttribute("role", "status");
5208
5755
  card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
5209
- 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);
5210
5758
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
5211
5759
  (this.els.sheetHead ?? this.els.side ?? this.els.map).appendChild(card);
5212
5760
  } else if (this.secCardCollapsed) {
5213
5761
  card.className = "sl-seccard mini on";
5214
5762
  card.setAttribute("role", "button");
5215
5763
  card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
5216
- 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);
5217
5766
  card.addEventListener("click", (e) => {
5218
5767
  if (e.target.closest(".sl-seccard-x")) return;
5219
5768
  this.secCardCollapsed = false;
@@ -5228,9 +5777,13 @@ var SeatPicker = class _SeatPicker {
5228
5777
  card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
5229
5778
  const mix = summary.categories.map((c) => {
5230
5779
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
5231
- 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>`;
5232
5784
  }).join("");
5233
- 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);
5234
5787
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
5235
5788
  card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
5236
5789
  (this.regions["top-center"] ?? this.els.map).appendChild(card);
@@ -5294,14 +5847,15 @@ var SeatPicker = class _SeatPicker {
5294
5847
  }
5295
5848
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
5296
5849
  const status = this.controller.getStatus(seat.id) ?? "free";
5297
- const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
5298
- const price = cat ? this.catPrice(cat) : void 0;
5850
+ const statusText = status === "free" ? this.tf("picker.statusAvailable", "available") : status === "held" ? this.tf("picker.statusOnHold", "on hold") : this.tf("picker.statusTaken2", "taken");
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;
5299
5853
  const table = this.controller.tableSelection(seat.id);
5300
5854
  const details = table ?? this.controller.seatDetails(seat.id);
5301
5855
  const typeWord = this.rowTypeWord(details);
5302
5856
  const buyerName = details?.rowLabel ?? details?.displayLabel ?? seat.displayLabel ?? seat.label;
5303
- const identity = table ? `${typeWord} ${buyerName}, ${table.bookingMode === "variable" ? `${table.minOccupancy} to ${table.maxOccupancy} guests` : `${table.capacity} guests`}` : details?.objectType === "booth" ? `${typeWord} ${buyerName}` : details?.objectType === "table" ? `${typeWord} ${buyerName}, seat ${details.seatNumber ?? seat.label}` : `Seat ${details?.displayLabel ?? seat.displayLabel ?? seat.label}`;
5304
- this.srEl.textContent = `${identity}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
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 });
5858
+ this.srEl.textContent = `${identity}, ${cat?.label ?? seat.categoryKey}${priceText ? `, ${priceText}` : ""}, ${statusText}`;
5305
5859
  }
5306
5860
  // ---- atomic table confirmation / guest quantity ---------------------------
5307
5861
  showTableDialog(table, held, returnFocus) {
@@ -5321,7 +5875,7 @@ var SeatPicker = class _SeatPicker {
5321
5875
  const typeWord = this.rowTypeWord(table);
5322
5876
  const el = document.createElement("div");
5323
5877
  el.className = "sl-table-scrim";
5324
- el.innerHTML = `<section class="sl-table-dialog" role="dialog" aria-modal="true" aria-labelledby="sl-table-title" aria-describedby="sl-table-copy"><div class="sl-table-head"><div class="sl-table-eyebrow">${esc(variable ? `Flexible party \xB7 ${typeWord}` : `Whole ${typeWord}`)}</div><h2 class="sl-table-title" id="sl-table-title">${esc(table.displayLabel ?? table.label)}</h2><p class="sl-table-copy" id="sl-table-copy">${variable ? `Choose how many guests will sit together. This table is held exclusively for your party.` : `All ${table.capacity} places are booked together as one exclusive table.`}</p></div><div class="sl-table-body"><div class="sl-table-summary"><span>${esc(cat?.label ?? table.categoryKey)}</span><b data-table-unit>${this.money(this.paidPrice(table.categoryKey, table.tierId ?? null, table.price))} per guest</b><span class="muted">Table capacity</span><span>${table.capacity} guests</span><span class="muted">Total</span><b data-table-total></b></div>` + (variable ? `<label class="sl-table-qtylabel" id="sl-table-qty-label">Number of guests</label><div class="sl-table-stepper" role="group" aria-labelledby="sl-table-qty-label"><button type="button" data-table-step="-1" aria-label="Fewer guests">\u2212</button><output aria-live="polite" data-table-qty>${table.quantity}</output><button type="button" data-table-step="1" aria-label="More guests">+</button></div><div class="sl-table-range">Choose ${table.minOccupancy}\u2013${table.maxOccupancy} guests</div>` : `<input type="hidden" data-table-qty value="${table.capacity}">`) + `<div class="sl-table-actions"><button type="button" class="sl-table-cancel">Cancel</button><button type="button" class="sl-table-confirm">${held ? "Update table" : variable ? "Select table" : "Select whole table"}</button></div></div></section>`;
5878
+ el.innerHTML = `<section class="sl-table-dialog" role="dialog" aria-modal="true" aria-labelledby="sl-table-title" aria-describedby="sl-table-copy"><div class="sl-table-head"><div class="sl-table-eyebrow">${esc(variable ? t2("picker.flexiblePartyTypeWord", { typeWord }) : t2("picker.wholeTypeWord", { typeWord }))}</div><h2 class="sl-table-title" id="sl-table-title">${esc(table.displayLabel ?? table.label)}</h2><p class="sl-table-copy" id="sl-table-copy">${variable ? this.tf("picker.chooseGuestsCopy", "Choose how many guests will sit together. This table is held exclusively for your party.") : t2("picker.allPlacesBookedTogether", { count: table.capacity })}</p></div><div class="sl-table-body"><div class="sl-table-summary"><span>${esc(cat?.label ?? table.categoryKey)}</span><b data-table-unit>${t2("picker.perGuestPrice", { price: this.money(this.paidPrice(table.categoryKey, table.tierId ?? null, table.price)) })}</b><span class="muted">${this.tf("picker.tableCapacity", "Table capacity")}</span><span>${t2("picker.guestsCount", { count: table.capacity })}</span><span class="muted">${this.tf("picker.total", "Total")}</span><b data-table-total></b></div>` + (variable ? `<label class="sl-table-qtylabel" id="sl-table-qty-label">${this.tf("picker.numberOfGuests", "Number of guests")}</label><div class="sl-table-stepper" role="group" aria-labelledby="sl-table-qty-label"><button type="button" data-table-step="-1" aria-label="${this.tf("picker.fewerGuests", "Fewer guests")}">\u2212</button><output aria-live="polite" data-table-qty>${table.quantity}</output><button type="button" data-table-step="1" aria-label="${this.tf("picker.moreGuests", "More guests")}">+</button></div><div class="sl-table-range">${t2("picker.chooseMinMaxGuests", { min: table.minOccupancy, max: table.maxOccupancy })}</div>` : `<input type="hidden" data-table-qty value="${table.capacity}">`) + `<div class="sl-table-actions"><button type="button" class="sl-table-cancel">${t2("common.cancel")}</button><button type="button" class="sl-table-confirm">${held ? this.tf("picker.updateTable", "Update table") : variable ? this.tf("picker.selectTable", "Select table") : this.tf("picker.selectWholeTable", "Select whole table")}</button></div></div></section>`;
5325
5879
  this.root.appendChild(el);
5326
5880
  this.tableDialogEl = el;
5327
5881
  this.renderTableDialogState();
@@ -5380,17 +5934,17 @@ var SeatPicker = class _SeatPicker {
5380
5934
  const button = this.tableDialogEl?.querySelector(".sl-table-confirm");
5381
5935
  if (button) {
5382
5936
  button.disabled = true;
5383
- button.textContent = held ? "Updating\u2026" : "Selecting\u2026";
5937
+ button.textContent = held ? this.tf("picker.updatingEllipsis", "Updating\u2026") : this.tf("picker.selectingEllipsis", "Selecting\u2026");
5384
5938
  }
5385
5939
  if (held) {
5386
5940
  try {
5387
5941
  const updated = await this.controller.replaceTableQuantity(table.label, table.quantity, this.opts.holdTtlMs);
5388
5942
  if (!updated) {
5389
- this.toast("That guest count could not be secured. Your current table hold is unchanged.", "warning");
5943
+ this.toast(this.tf("picker.guestCountCouldNotBeSecured", "That guest count could not be secured. Your current table hold is unchanged."), "warning");
5390
5944
  this.renderTableDialogState();
5391
5945
  if (button) {
5392
5946
  button.disabled = false;
5393
- button.textContent = "Update table";
5947
+ button.textContent = this.tf("picker.updateTable", "Update table");
5394
5948
  }
5395
5949
  return;
5396
5950
  }
@@ -5404,13 +5958,13 @@ var SeatPicker = class _SeatPicker {
5404
5958
  this.dismissTableDialog();
5405
5959
  this.syncTray();
5406
5960
  this.emitHoldChange();
5407
- this.toast(`${table.label} updated for ${table.quantity} guests.`, "success");
5961
+ this.toast(t2("picker.tableUpdatedForGuests", { label: table.label, count: table.quantity }), "success");
5408
5962
  } catch (error) {
5409
5963
  this.opts.onError?.(error);
5410
- this.toast("That guest count is no longer available. Your current hold is unchanged.", "error");
5964
+ this.toast(this.tf("picker.guestCountNoLongerAvailable", "That guest count is no longer available. Your current hold is unchanged."), "error");
5411
5965
  if (button) {
5412
5966
  button.disabled = false;
5413
- button.textContent = "Update table";
5967
+ button.textContent = this.tf("picker.updateTable", "Update table");
5414
5968
  }
5415
5969
  }
5416
5970
  return;
@@ -5418,7 +5972,7 @@ var SeatPicker = class _SeatPicker {
5418
5972
  if (!this.controller.setTableQuantity(table.label, table.quantity)) {
5419
5973
  if (button) {
5420
5974
  button.disabled = false;
5421
- button.textContent = table.bookingMode === "variable" ? "Select table" : "Select whole table";
5975
+ button.textContent = table.bookingMode === "variable" ? this.tf("picker.selectTable", "Select table") : this.tf("picker.selectWholeTable", "Select whole table");
5422
5976
  }
5423
5977
  return;
5424
5978
  }
@@ -5464,16 +6018,26 @@ var SeatPicker = class _SeatPicker {
5464
6018
  '"': "&quot;"
5465
6019
  })[char]);
5466
6020
  const identityFields = [
5467
- details?.sectionLabel ? `<div class="sl-confirm-field"><span class="sl-confirm-key">Section</span><span class="sl-confirm-value">${safe(details.sectionLabel)}</span></div>` : "",
6021
+ details?.sectionLabel ? `<div class="sl-confirm-field"><span class="sl-confirm-key">${this.tf("picker.section", "Section")}</span><span class="sl-confirm-value">${safe(details.sectionLabel)}</span></div>` : "",
5468
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>` : "",
5469
- details?.objectType !== "booth" ? `<div class="sl-confirm-field"><span class="sl-confirm-key">Seat</span><span class="sl-confirm-value">${safe(details?.seatNumber ?? details?.displayLabel ?? seat.displayLabel ?? seat.label)}</span></div>` : ""
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>` : ""
5470
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>` : "";
5471
6032
  const el = document.createElement("div");
5472
6033
  el.className = "sl-confirm";
5473
6034
  el.setAttribute("role", "dialog");
5474
- el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
5475
- el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
5476
- 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">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>Select</button></div></div>`;
6035
+ el.setAttribute("aria-label", t2("picker.confirmSeatLabel", { label: seat.label }));
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>`;
5477
6041
  this.els.map.appendChild(el);
5478
6042
  this.confirmEl = el;
5479
6043
  const thumb = el.querySelector(".sl-confirm-thumb");
@@ -5489,6 +6053,22 @@ var SeatPicker = class _SeatPicker {
5489
6053
  this.openSeatConfidencePassport(seat, event.currentTarget instanceof HTMLElement ? event.currentTarget : null);
5490
6054
  });
5491
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
+ });
5492
6072
  el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
5493
6073
  if (this.buyerView === "venue3d") {
5494
6074
  this.dismissConfirm();
@@ -5626,7 +6206,7 @@ var SeatPicker = class _SeatPicker {
5626
6206
  el.className = "sl-view";
5627
6207
  el.setAttribute("role", "dialog");
5628
6208
  el.setAttribute("aria-label", t2("picker.viewFromSeat", { label: seat.label }));
5629
- el.innerHTML = `<div class="sl-view-head"><span class="sl-view-title">${t2("picker.viewFromSeat", { label: seat.label })}</span><span class="sl-view-cap">${caption}</span><button type="button" class="sl-view-x" aria-label="Close"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div><div class="sl-view-pano"><span class="sl-view-badge">${real ? t2("picker.real360") : t2("picker.preview")}</span><span class="sl-view-hint">Drag to look around \xB7 scroll to zoom</span></div>`;
6209
+ el.innerHTML = `<div class="sl-view-head"><span class="sl-view-title">${t2("picker.viewFromSeat", { label: seat.label })}</span><span class="sl-view-cap">${caption}</span><button type="button" class="sl-view-x" aria-label="${this.tf("picker.close", "Close")}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div><div class="sl-view-pano"><span class="sl-view-badge">${real ? t2("picker.real360") : t2("picker.preview")}</span><span class="sl-view-hint">${this.tf("picker.dragToLookAround", "Drag to look around \xB7 scroll to zoom")}</span></div>`;
5630
6210
  this.root.appendChild(el);
5631
6211
  this.viewEl = el;
5632
6212
  const pano = el.querySelector(".sl-view-pano");
@@ -5822,27 +6402,27 @@ var SeatPicker = class _SeatPicker {
5822
6402
  copy.className = "sl-offer-copy";
5823
6403
  const kicker = document.createElement("span");
5824
6404
  kicker.className = "sl-offer-kicker";
5825
- kicker.textContent = active ? "Current ticket offer" : "Upcoming ticket offer";
6405
+ kicker.textContent = active ? this.tf("picker.currentTicketOffer", "Current ticket offer") : this.tf("picker.upcomingTicketOffer", "Upcoming ticket offer");
5826
6406
  const name = document.createElement("strong");
5827
6407
  name.className = "sl-offer-name";
5828
- name.textContent = offer.name || (active ? "Current offer" : "Scheduled offer");
6408
+ name.textContent = offer.name || (active ? this.tf("picker.currentOffer", "Current offer") : this.tf("picker.scheduledOffer", "Scheduled offer"));
5829
6409
  const line = document.createElement("span");
5830
6410
  line.className = "sl-offer-line";
5831
6411
  const facts = [];
5832
6412
  if (active && availability.fromPrice != null) facts.push(this.money(availability.fromPrice / 100));
5833
- if (active && offer.remaining != null) facts.push(`${offer.remaining} available`);
5834
- if (active && offer.endsAt != null) facts.push(`until ${formatWhen(offer.endsAt, this.eventTimezone, this.opts.locale)}`);
5835
- if (upcoming?.startsAt != null) facts.push(`starts ${formatWhen(upcoming.startsAt, this.eventTimezone, this.opts.locale)}`);
6413
+ if (active && offer.remaining != null) facts.push(t2("picker.offerRemainingAvailable", { count: offer.remaining }));
6414
+ if (active && offer.endsAt != null) facts.push(t2("picker.offerUntil", { time: formatWhen(offer.endsAt, this.eventTimezone, this.opts.locale) }));
6415
+ if (upcoming?.startsAt != null) facts.push(t2("picker.offerStarts", { time: formatWhen(upcoming.startsAt, this.eventTimezone, this.opts.locale) }));
5836
6416
  line.textContent = facts.join(" \xB7 ");
5837
6417
  copy.append(kicker, name, line);
5838
6418
  const info = document.createElement("details");
5839
6419
  info.className = "sl-offer-info";
5840
6420
  const summary = document.createElement("summary");
5841
- summary.setAttribute("aria-label", `How the ${name.textContent} offer works`);
6421
+ summary.setAttribute("aria-label", t2("picker.howOfferWorks", { name: name.textContent ?? "" }));
5842
6422
  summary.textContent = "i";
5843
6423
  const detail = document.createElement("div");
5844
6424
  detail.className = "sl-offer-detail";
5845
- detail.textContent = active ? `This price applies automatically to eligible seats. Tickets in active carts temporarily reduce the available quantity; released or expired holds return it. When the offer ends, the next matching offer or normal ticket price takes over.` : `Tickets are available at their normal price now. This scheduled offer will apply automatically to eligible seats when it starts.`;
6425
+ detail.textContent = active ? this.tf("picker.offerDetailActive", "This price applies automatically to eligible seats. Tickets in active carts temporarily reduce the available quantity; released or expired holds return it. When the offer ends, the next matching offer or normal ticket price takes over.") : this.tf("picker.offerDetailUpcoming", "Tickets are available at their normal price now. This scheduled offer will apply automatically to eligible seats when it starts.");
5846
6426
  info.append(summary, detail);
5847
6427
  main.append(copy, info);
5848
6428
  host.replaceChildren(main);
@@ -5865,21 +6445,23 @@ var SeatPicker = class _SeatPicker {
5865
6445
  const doc = this.controller.doc;
5866
6446
  if (!doc || !this.els.prices) return;
5867
6447
  const left = this.controller.categoryAvailability();
5868
- this.narrateAvailability(doc.categories, left);
5869
- this.syncSoldout(doc.categories, left);
6448
+ const sellable = doc.categories.filter((c) => !c.notForSale);
6449
+ this.narrateAvailability(sellable, left);
6450
+ this.syncSoldout(sellable, left);
5870
6451
  const PRICE_LIMIT = 5;
5871
- const overflow = doc.categories.length - PRICE_LIMIT;
6452
+ const overflow = sellable.length - PRICE_LIMIT;
5872
6453
  const collapsed = overflow > 1 && !this.pricesExpanded;
5873
- const shown = collapsed ? doc.categories.slice(0, PRICE_LIMIT) : doc.categories;
6454
+ const shown = collapsed ? sellable.slice(0, PRICE_LIMIT) : sellable;
5874
6455
  this.els.prices.classList.toggle("sl-expanded", overflow > 1 && this.pricesExpanded);
5875
6456
  this.els.prices.innerHTML = shown.map((c) => {
5876
6457
  const price = this.catPrice(c);
6458
+ const range = this.catPriceRange(c);
5877
6459
  const offer = this.offerPrice(c.key);
5878
6460
  const previous = offer?.previousPrice != null && offer.previousPrice > offer.price ? offer.previousPrice : null;
5879
6461
  const active = this.focusedCatKey === c.key;
5880
6462
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
5881
- 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 ? "Show all seats" : `Show ${c.label} seats on the map`)}"><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">${left[c.key] ?? 0} left</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>`;
5882
- }).join("") + (overflow > 1 ? `<button type="button" class="sl-price-more" aria-expanded="${!collapsed}">` + (collapsed ? `Show all ${doc.categories.length} ticket types` : "Show fewer") + `</button>` : "") + `<div class="sl-status-key" aria-label="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>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>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>`;
5883
6465
  this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
5884
6466
  row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
5885
6467
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
@@ -5937,7 +6519,7 @@ var SeatPicker = class _SeatPicker {
5937
6519
  const now = left[cat.key] ?? 0;
5938
6520
  if (before === void 0 || now >= before) continue;
5939
6521
  const taken = before - now;
5940
- textEl.textContent = `${taken} seat${taken === 1 ? "" : "s"} just taken in ${cat.label} \xB7 ${now} left`;
6522
+ textEl.textContent = tCount("picker.seatsJustTakenInCategory", taken, { label: cat.label, left: now });
5941
6523
  this.els.live?.classList.remove("on");
5942
6524
  void this.els.live?.offsetWidth;
5943
6525
  this.els.live?.classList.add("on");
@@ -5955,12 +6537,17 @@ var SeatPicker = class _SeatPicker {
5955
6537
  const gone = this.controller.getSelection().filter((s) => !ownLabels.has(s.label) && (this.controller.getStatus(s.id) ?? "free") !== "free");
5956
6538
  if (!gone.length) return;
5957
6539
  this.controller.deselect(gone.map((s) => s.id));
5958
- this.toast(`Seat ${gone[0].label} was just taken by another buyer.`, "error");
6540
+ this.toast(t2("picker.seatJustTakenByAnother", { label: gone[0].label }), "error");
5959
6541
  }
5960
6542
  syncTray() {
5961
6543
  if (!this.els.tray) return;
5962
6544
  this.updateSelectionCapacity();
5963
6545
  const seats = this.committedSelection();
6546
+ if (this.root?.dataset.layout === "narrow" && this.secCardEl && (this.hold || seats.length || this.pendingGACount())) {
6547
+ this.secCardEl.remove();
6548
+ this.secCardEl = null;
6549
+ this.lastSection = null;
6550
+ }
5964
6551
  const gaAreas = this.controller.getGAAreas();
5965
6552
  const heldItems = this.hold?.items ?? [];
5966
6553
  const parts = [];
@@ -5971,18 +6558,22 @@ var SeatPicker = class _SeatPicker {
5971
6558
  `<div class="sl-closed-note" role="status"><b>${this.tf("picker.salesClosedPill", "Sales are closed")}</b><span>${this.tf("picker.salesClosedCopy", "Ticket sales for this event have ended.")}${closedWhen}</span></div>`
5972
6559
  );
5973
6560
  } else if (!seats.length && !heldItems.length && !gaAreas.length) {
5974
- parts.push(`<div class="sl-tray-hint">Tap a seat on the map, or let us pick the best available for you.</div>`);
6561
+ parts.push(`<div class="sl-tray-hint">${this.tf("picker.trayHintTapOrBest", "Tap a seat on the map, or let us pick the best available for you.")}</div>`);
5975
6562
  } else if (!seats.length && !heldItems.length) {
5976
- parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
6563
+ parts.push(`<div class="sl-tray-hint">${this.tf("picker.trayHintTapOrStanding", "Tap a seat on the map \u2014 or grab standing tickets below.")}</div>`);
5977
6564
  }
5978
6565
  const noPicks = !seats.length && !heldItems.length && !this.pendingGACount();
5979
- if (!this.salesClosed && !this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
5980
- const cats = this.controller.doc?.categories ?? [];
6566
+ if (noPicks) this.baReopen = false;
6567
+ if (!this.salesClosed && !this.hold && (noPicks || this.baReopen || this.bestAvailableBusy || this.bestAvailableConfirm)) {
6568
+ const cats = (this.controller.doc?.categories ?? []).filter((c) => !c.notForSale);
5981
6569
  const zones = this.controller.getBestAvailableZones();
5982
6570
  if (this.baZone && !zones.some((zone) => zone.id === this.baZone)) this.baZone = "";
5983
- parts.push(this.bestAvailableConfirm ? `<div class="sl-ba" role="alert"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Replace your current choices?</div><div class="sl-ba-replace"><b>We\u2019ll find ${this.baQty} seats together.</b><span>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>Keep mine</button><button type="button" class="replace" data-ba-replace>Find new seats</button></div></div>` : `<div class="sl-ba"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Find the best seats together</div><div class="sl-ba-copy"><span class="wide">We\u2019ll choose the closest available group for you.</span><span class="narrow">Closest available group, chosen instantly.</span></div>` + // Premium quick-pick — present only when the chart actually has premium
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
5984
6572
  // seats (same present-only philosophy as the a11y filter chips).
5985
- (this.controller.hasPremiumSeats() ? `<button type="button" class="sl-ba-premium${this.baPremium ? " on" : ""}" data-ba-premium aria-pressed="${this.baPremium ? "true" : "false"}"><span class="star" aria-hidden="true">\u2605</span>${this.tf("picker.bestSeatsPremium", "Best seats")}</button>` : "") + (cats.length > 1 ? `<select aria-label="Preferred ticket type" data-ba-cat><option value="">Any ticket type</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : `<span aria-hidden="true"></span>`) + (zones.length ? `<select aria-label="Preferred venue zone" data-ba-zone><option value="">Any venue zone</option>` + zones.map((zone) => `<option value="${escapeOption(zone.id)}"${this.baZone === zone.id ? " selected" : ""}>${escapeOption(zone.label)}</option>`).join("") + `</select>` : "") + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go${this.bestAvailableBusy ? " sl-busy" : ""}"${this.bestAvailableBusy ? " disabled" : ""}>` + (this.bestAvailableBusy ? `<span class="sl-ba-spin" aria-hidden="true"></span>Finding the best seats\u2026` : `Find ${this.baQty} best ${this.baQty === 1 ? "seat" : "seats"}`) + `</button></div>`);
6573
+ (this.controller.hasPremiumSeats() ? `<button type="button" class="sl-ba-premium${this.baPremium ? " on" : ""}" data-ba-premium aria-pressed="${this.baPremium ? "true" : "false"}"><span class="star" aria-hidden="true">\u2605</span>${this.tf("picker.bestSeatsPremium", "Best seats")}</button>` : "") + (cats.length > 1 ? `<select aria-label="${this.tf("picker.preferredTicketType", "Preferred ticket type")}" data-ba-cat><option value="">${this.tf("picker.anyTicketType", "Any ticket type")}</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : `<span aria-hidden="true"></span>`) + (zones.length ? `<select aria-label="${this.tf("picker.preferredVenueZone", "Preferred venue zone")}" data-ba-zone><option value="">${this.tf("picker.anyVenueZone", "Any venue zone")}</option>` + zones.map((zone) => `<option value="${escapeOption(zone.id)}"${this.baZone === zone.id ? " selected" : ""}>${escapeOption(zone.label)}</option>`).join("") + `</select>` : "") + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="${this.tf("picker.fewerSeats", "Fewer seats")}">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="${this.tf("picker.moreSeats", "More seats")}">+</button></div><button type="button" class="sl-ba-go${this.bestAvailableBusy ? " sl-busy" : ""}"${this.bestAvailableBusy ? " disabled" : ""}>` + (this.bestAvailableBusy ? `<span class="sl-ba-spin" aria-hidden="true"></span>${this.tf("picker.findingBestSeats", "Finding the best seats\u2026")}` : tCount("picker.findBestSeatsCount", this.baQty)) + `</button>` + // Reopened over an existing selection: the buyer needs the way back
6574
+ // stated as plainly as the way forward (Go routes through the
6575
+ // "Replace your current choices?" confirm, so nothing is lost yet).
6576
+ (this.baReopen ? `<button type="button" class="sl-ba-back" data-ba-close>${this.tf("picker.keepMyPicks", "Keep my picks")}</button>` : "") + `</div>`);
5986
6577
  }
5987
6578
  const idGrid = (seatId, label, objectType, quantity = 1, objectId, identity) => {
5988
6579
  const esc = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
@@ -5994,21 +6585,21 @@ var SeatPicker = class _SeatPicker {
5994
6585
  const d = seatId ? this.controller.seatDetails(seatId) : null;
5995
6586
  const area = objectType === "ga" ? gaAreas.find((candidate) => candidate.id === objectId) : void 0;
5996
6587
  const effectiveType = objectType === "ga" ? "ga" : d?.objectType ?? objectType;
5997
- const typeWord = identity?.displayType?.trim() || d?.displayType?.trim() || area?.displayType?.trim() || (effectiveType === "table" ? "Table" : effectiveType === "booth" ? "Booth" : effectiveType === "ga" ? "General admission" : "Row");
6588
+ const typeWord = identity?.displayType?.trim() || d?.displayType?.trim() || area?.displayType?.trim() || (effectiveType === "table" ? this.tf("picker.table", "Table") : effectiveType === "booth" ? this.tf("picker.booth", "Booth") : effectiveType === "ga" ? this.tf("picker.generalAdmission", "General admission") : this.tf("picker.row", "Row"));
5998
6589
  const buyerName = identity?.rowLabel ?? identity?.displayLabel ?? d?.rowLabel ?? d?.displayLabel ?? area?.displayLabel ?? area?.label ?? label;
5999
6590
  if (effectiveType === "table" && identity?.bookingMode) {
6000
- return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">${esc(typeWord)}</span><span class="val">${esc(buyerName)}</span></span><span class="fld mid"><span class="sl-chip-eb">Guests</span><span class="val">${quantity}</span></span></div>`;
6591
+ return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">${esc(typeWord)}</span><span class="val">${esc(buyerName)}</span></span><span class="fld mid"><span class="sl-chip-eb">${this.tf("picker.guests", "Guests")}</span><span class="val">${quantity}</span></span></div>`;
6001
6592
  }
6002
6593
  if (effectiveType === "ga") {
6003
- return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">${esc(typeWord)}</span><span class="val">${esc(buyerName)}</span></span>` + (quantity > 1 ? `<span class="fld mid"><span class="sl-chip-eb">Tickets</span><span class="val">${quantity}</span></span>` : "") + `</div>`;
6594
+ return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">${esc(typeWord)}</span><span class="val">${esc(buyerName)}</span></span>` + (quantity > 1 ? `<span class="fld mid"><span class="sl-chip-eb">${this.tf("picker.tickets", "Tickets")}</span><span class="val">${quantity}</span></span>` : "") + `</div>`;
6004
6595
  }
6005
6596
  if (effectiveType === "booth") {
6006
- return `<div class="sl-chip-id">` + (d?.sectionLabel ? `<span class="fld sec"><span class="sl-chip-eb">Section</span><span class="val">${esc(d.sectionLabel)}</span></span>` : "") + `<span class="fld mid"><span class="sl-chip-eb">${esc(typeWord)}</span><span class="val">${esc(buyerName)}</span></span></div>`;
6597
+ return `<div class="sl-chip-id">` + (d?.sectionLabel ? `<span class="fld sec"><span class="sl-chip-eb">${this.tf("picker.section", "Section")}</span><span class="val">${esc(d.sectionLabel)}</span></span>` : "") + `<span class="fld mid"><span class="sl-chip-eb">${esc(typeWord)}</span><span class="val">${esc(buyerName)}</span></span></div>`;
6007
6598
  }
6008
6599
  if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
6009
- return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${esc(buyerName)}</span></span></div>`;
6600
+ return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">${this.tf("picker.seat", "Seat")}</span><span class="val">${esc(buyerName)}</span></span></div>`;
6010
6601
  }
6011
- return `<div class="sl-chip-id">` + (d.sectionLabel ? `<span class="fld sec"><span class="sl-chip-eb">Section</span><span class="val">${esc(d.sectionLabel)}</span></span>` : "") + (d.rowLabel ? `<span class="fld mid"><span class="sl-chip-eb">${esc(typeWord)}</span><span class="val">${esc(this.rowShort(d))}</span></span>` : "") + (d.seatNumber ? `<span class="fld mid"><span class="sl-chip-eb">Seat</span><span class="val">${esc(d.seatNumber)}</span></span>` : "") + `</div>`;
6602
+ return `<div class="sl-chip-id">` + (d.sectionLabel ? `<span class="fld sec"><span class="sl-chip-eb">${this.tf("picker.section", "Section")}</span><span class="val">${esc(d.sectionLabel)}</span></span>` : "") + (d.rowLabel ? `<span class="fld mid"><span class="sl-chip-eb">${esc(typeWord)}</span><span class="val">${esc(this.rowShort(d))}</span></span>` : "") + (d.seatNumber ? `<span class="fld mid"><span class="sl-chip-eb">${this.tf("picker.seat", "Seat")}</span><span class="val">${esc(d.seatNumber)}</span></span>` : "") + `</div>`;
6012
6603
  };
6013
6604
  const iconRail = (rmAria, viewLabel) => `<div class="sl-chip-rail"><button type="button" class="rm" aria-label="${rmAria}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>` + (viewLabel ? `<button type="button" class="view" data-view-label="${viewLabel}" aria-label="${t2("picker.viewFromSeat", { label: viewLabel })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") + `</div>`;
6014
6605
  for (const item of heldItems) {
@@ -6020,7 +6611,7 @@ var SeatPicker = class _SeatPicker {
6020
6611
  const table = item.objectType === "table" ? this.controller.tableSelection(item.label) : null;
6021
6612
  const canView2 = this.seatViewEnabled() && !!heldSeat && item.objectType !== "table";
6022
6613
  parts.push(
6023
- `<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-held="${encodeURIComponent(item.label)}"${heldSeat ? ` data-locate="${heldSeat.id}"` : ""}><div class="sl-chip-main">` + idGrid(heldSeat?.id ?? null, item.label, item.objectType, item.quantity ?? 1, item.objectId, table ?? void 0) + `<div class="sl-chip-sub"><span class="sl-ticket-state held" aria-label="Held for you" title="Held for you"><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></span><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span>` + (table?.bookingMode === "variable" ? `<button type="button" class="sl-table-edit" data-table-edit="${encodeURIComponent(item.label)}">${item.quantity ?? 1} guests \xB7 Edit</button>` : "") + this.wheelchairChipMarker(heldSeat?.wheelchairSpaceType) + this.commercialChipMarker(heldSeat?.commercial) + `<span class="amt">${this.money(this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1))}</span></div></div>` + iconRail(`Remove held ticket ${item.label}`, canView2 ? item.label : null) + `</div>`
6614
+ `<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-held="${encodeURIComponent(item.label)}"${heldSeat ? ` data-locate="${heldSeat.id}"` : ""}><div class="sl-chip-main">` + idGrid(heldSeat?.id ?? null, item.label, item.objectType, item.quantity ?? 1, item.objectId, table ?? void 0) + `<div class="sl-chip-sub"><span class="sl-ticket-state held" aria-label="${this.tf("picker.heldForYou", "Held for you")}" title="${this.tf("picker.heldForYou", "Held for you")}"><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></span><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span>` + (table?.bookingMode === "variable" ? `<button type="button" class="sl-table-edit" data-table-edit="${encodeURIComponent(item.label)}">${t2("picker.guestsCountEdit", { count: item.quantity ?? 1 })}</button>` : "") + this.wheelchairChipMarker(heldSeat?.wheelchairSpaceType) + this.commercialChipMarker(heldSeat?.commercial) + `<span class="amt">${this.money(this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1))}</span></div></div>` + iconRail(t2("picker.removeHeldTicketLabel", { label: item.label }), canView2 ? item.label : null) + `</div>`
6024
6615
  );
6025
6616
  }
6026
6617
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -6030,14 +6621,21 @@ var SeatPicker = class _SeatPicker {
6030
6621
  nextTrayKeys.add(itemKey);
6031
6622
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
6032
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") : "");
6033
6626
  parts.push(
6034
- `<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="Selected" title="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)}">${s.quantity ?? 1} guests \xB7 Edit</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(`Remove ${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>`
6035
6628
  );
6036
6629
  }
6037
6630
  for (const area of gaAreas) {
6038
6631
  const qty = this.gaQty.get(area.id) ?? 0;
6039
6632
  parts.push(
6040
- `<div class="sl-ga" data-ga="${area.id}"><div class="sl-ga-info"><div class="sl-ga-name">${area.displayLabel ?? area.label}</div><div class="sl-ga-sub">${area.displayType ?? "General admission"} \xB7 ${this.money(this.paidPrice(area.categoryKey, null, area.price))} \xB7 ${area.available} left</div></div><div class="sl-ga-qty"><button type="button" data-d="-1" aria-label="Fewer">\u2212</button><span>${qty}</span><button type="button" data-d="1" aria-label="More">+</button></div></div>`
6633
+ `<div class="sl-ga" data-ga="${area.id}"><div class="sl-ga-info"><div class="sl-ga-name">${area.displayLabel ?? area.label}</div><div class="sl-ga-sub">${area.displayType ?? this.tf("picker.generalAdmission", "General admission")} \xB7 ${this.money(this.paidPrice(area.categoryKey, null, area.price))} \xB7 ${tCount("picker.leftCount", area.available)}</div></div><div class="sl-ga-qty"><button type="button" data-d="-1" aria-label="${this.tf("picker.fewer", "Fewer")}">\u2212</button><span>${qty}</span><button type="button" data-d="1" aria-label="${this.tf("picker.more", "More")}">+</button></div></div>`
6634
+ );
6635
+ }
6636
+ if (!this.salesClosed && !this.hold && !noPicks && !this.baReopen && !this.bestAvailableBusy && !this.bestAvailableConfirm) {
6637
+ parts.push(
6638
+ `<button type="button" class="sl-ba-reopen" data-ba-reopen><span aria-hidden="true">\u2726</span>${this.tf("picker.findTogetherInstead", "Find seats together instead")}</button>`
6041
6639
  );
6042
6640
  }
6043
6641
  this.els.tray.innerHTML = parts.join("");
@@ -6072,6 +6670,16 @@ var SeatPicker = class _SeatPicker {
6072
6670
  this.syncTray();
6073
6671
  this.els.tray.querySelector(".sl-ba-go")?.focus();
6074
6672
  });
6673
+ this.els.tray.querySelector("[data-ba-reopen]")?.addEventListener("click", () => {
6674
+ this.baReopen = true;
6675
+ this.syncTray();
6676
+ this.els.tray.querySelector(".sl-ba-go")?.focus();
6677
+ });
6678
+ this.els.tray.querySelector("[data-ba-close]")?.addEventListener("click", () => {
6679
+ this.baReopen = false;
6680
+ this.syncTray();
6681
+ this.els.tray.querySelector("[data-ba-reopen]")?.focus();
6682
+ });
6075
6683
  this.els.tray.querySelector("[data-ba-replace]")?.addEventListener("click", () => {
6076
6684
  this.bestAvailableConfirm = false;
6077
6685
  void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium, zoneId: this.baZone || void 0 });
@@ -6084,15 +6692,15 @@ var SeatPicker = class _SeatPicker {
6084
6692
  return;
6085
6693
  }
6086
6694
  const id = chip.dataset.seat;
6087
- const label = this.controller.getSelection().find((sel) => sel.id === id)?.label ?? "Seat";
6695
+ const label = this.controller.getSelection().find((sel) => sel.id === id)?.label ?? this.tf("picker.seat", "Seat");
6088
6696
  const remove = () => {
6089
6697
  this.controller.deselect([id]);
6090
- this.toast(`${label} removed.`, "neutral", {
6091
- label: "Undo",
6698
+ this.toast(t2("picker.labelRemoved", { label }), "neutral", {
6699
+ label: this.tf("picker.undo", "Undo"),
6092
6700
  onClick: () => {
6093
6701
  const restored = this.controller.select([id]);
6094
6702
  this.toast(
6095
- restored.length ? `${label} restored.` : `${label} is no longer available.`,
6703
+ restored.length ? t2("picker.labelRestored", { label }) : t2("picker.labelNoLongerAvailable", { label }),
6096
6704
  restored.length ? "success" : "warning"
6097
6705
  );
6098
6706
  }
@@ -6164,31 +6772,31 @@ var SeatPicker = class _SeatPicker {
6164
6772
  const pendingCount = this.pendingSelectionCount();
6165
6773
  const previousCount = this.lastTrayCount;
6166
6774
  const previousTotal = this.lastTrayTotal;
6167
- this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
6775
+ this.els.count.textContent = count ? tCount("picker.ticketsCount", count) : this.tf("picker.noSeatsSelected", "No seats selected");
6168
6776
  this.els.total.textContent = count ? this.money(total) : "";
6169
6777
  this.root?.setAttribute("data-has-selection", String(count > 0));
6170
6778
  this.root?.setAttribute(
6171
6779
  "data-ba-active",
6172
- String(this.bestAvailableConfirm || this.bestAvailableBusy)
6780
+ String(this.bestAvailableConfirm || this.bestAvailableBusy || this.baReopen)
6173
6781
  );
6174
6782
  this.els.foot?.classList.toggle("empty", count === 0);
6175
6783
  if (this.els.seatSummary) {
6176
- this.els.seatSummary.textContent = count ? `${count} selected` : "";
6784
+ this.els.seatSummary.textContent = count ? tCount("picker.seatsSelectedCount", count) : "";
6177
6785
  }
6178
6786
  this.syncCta(count, pendingCount);
6179
6787
  if (this.hold) {
6180
6788
  const securedCount = heldCount || this.hold.seats?.length || 0;
6181
6789
  if (this.els.holdTitle) {
6182
- this.els.holdTitle.textContent = `${securedCount} secured`;
6790
+ this.els.holdTitle.textContent = t2("picker.securedCount", { count: securedCount });
6183
6791
  }
6184
6792
  if (this.els.holdCopy) {
6185
6793
  this.holdCopyPending = pendingCount > 0;
6186
- this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected` : this.holdReassuranceText();
6794
+ this.els.holdCopy.textContent = pendingCount ? t2("picker.moreSelectedCount", { count: pendingCount }) : this.holdReassuranceText();
6187
6795
  }
6188
6796
  const change = this.els.holdChange;
6189
6797
  if (change) {
6190
6798
  change.disabled = this.releasingHold;
6191
- change.textContent = this.releasingHold ? "Releasing\u2026" : "Change";
6799
+ change.textContent = this.releasingHold ? this.tf("picker.releasingEllipsis", "Releasing\u2026") : this.tf("picker.change", "Change");
6192
6800
  }
6193
6801
  }
6194
6802
  if (count !== previousCount) this.animateOnce(this.els.count, "sl-value-pop", 380);
@@ -6213,21 +6821,21 @@ var SeatPicker = class _SeatPicker {
6213
6821
  renderPeek(count, total, pendingCount) {
6214
6822
  if (!this.els.peek) return;
6215
6823
  if (count && this.ctaPhase === "holding") {
6216
- this.els.peek.innerHTML = `<span>Securing your seats\u2026</span>`;
6824
+ this.els.peek.innerHTML = `<span>${this.tf("picker.securingSeats", "Securing your seats\u2026")}</span>`;
6217
6825
  return;
6218
6826
  }
6219
6827
  if (count && this.ctaPhase === "checkout") {
6220
- this.els.peek.innerHTML = `<span>\u2713 ${count} ${count === 1 ? "ticket" : "tickets"} secured \xB7 ${this.money(total)} \u2014 you won\u2019t be charged yet</span>`;
6828
+ this.els.peek.innerHTML = `<span>${t2("picker.peekSecured", { count, total: this.money(total) })}</span>`;
6221
6829
  return;
6222
6830
  }
6223
6831
  if (count) {
6224
- const holding = !!this.hold && !pendingCount;
6225
- this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><button type="button" class="go sl-sheet-go" data-act="${holding ? "checkout" : "open"}">${this.hold ? pendingCount ? "Secure more" : "Continue" : "Review"}</button>`;
6832
+ const toCheckout = !!this.hold;
6833
+ this.els.peek.innerHTML = `<span>${tCount("picker.peekTicketsTotal", count, { total: this.money(total) })}</span><button type="button" class="go sl-sheet-go" data-act="${toCheckout ? "checkout" : "open"}">${this.hold ? pendingCount ? this.tf("picker.secureMore", "Secure more") : this.tf("picker.continue", "Continue") : this.tf("picker.review", "Review")}</button>`;
6226
6834
  } else if (this.salesClosed) {
6227
6835
  this.els.peek.innerHTML = `<span>${this.tf("picker.salesClosedPill", "Sales are closed")}</span>`;
6228
6836
  } else {
6229
6837
  const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
6230
- this.els.peek.innerHTML = (prices.length ? `<span>From ${this.money(Math.min(...prices))}</span>` : "<span>Pick your seats</span>") + `<button type="button" class="go sl-sheet-go" data-act="open">\u2726 Best seats</button>`;
6838
+ this.els.peek.innerHTML = (prices.length ? `<span>${t2("picker.peekFromPrice", { price: this.money(Math.min(...prices)) })}</span>` : `<span>${this.tf("picker.pickYourSeats", "Pick your seats")}</span>`) + `<button type="button" class="go sl-sheet-go" data-act="open">${this.tf("picker.bestSeatsStar", "\u2726 Best seats")}</button>`;
6231
6839
  }
6232
6840
  }
6233
6841
  async removeHeldLabel(label, chip) {
@@ -6240,7 +6848,7 @@ var SeatPicker = class _SeatPicker {
6240
6848
  const preserveAcrossNavigation = this.handedOff;
6241
6849
  const released = await this.controller.releaseLabels([label]);
6242
6850
  if (!released) {
6243
- this.toast(`Couldn't remove ${label}. Your hold is unchanged.`, "error");
6851
+ this.toast(t2("picker.couldNotRemoveLabel", { label }), "error");
6244
6852
  return false;
6245
6853
  }
6246
6854
  const remaining = this.controller.currentHold();
@@ -6256,7 +6864,7 @@ var SeatPicker = class _SeatPicker {
6256
6864
  }
6257
6865
  this.syncTray();
6258
6866
  this.emitHoldChange();
6259
- this.toast(`${label} removed from your hold.`, "success");
6867
+ this.toast(t2("picker.labelRemovedFromHold", { label }), "success");
6260
6868
  return true;
6261
6869
  } finally {
6262
6870
  this.releasingLabels.delete(label);
@@ -6270,23 +6878,23 @@ var SeatPicker = class _SeatPicker {
6270
6878
  const button = this.els.holdChange;
6271
6879
  if (button) {
6272
6880
  button.disabled = true;
6273
- button.textContent = "Releasing\u2026";
6881
+ button.textContent = this.tf("picker.releasingEllipsis", "Releasing\u2026");
6274
6882
  }
6275
6883
  try {
6276
6884
  await this.release();
6277
- if (!this.hold) this.toast("Held tickets released. Choose your new seats.", "success");
6885
+ if (!this.hold) this.toast(this.tf("picker.heldTicketsReleased", "Held tickets released. Choose your new seats."), "success");
6278
6886
  } finally {
6279
6887
  this.releasingHold = false;
6280
6888
  if (button?.isConnected) {
6281
6889
  button.disabled = false;
6282
- button.textContent = "Change";
6890
+ button.textContent = this.tf("picker.change", "Change");
6283
6891
  }
6284
6892
  }
6285
6893
  }
6286
6894
  async handleCta() {
6287
6895
  if (this.salesClosed) return;
6288
6896
  if (this.totalTicketCount() > this.maxTickets) {
6289
- this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
6897
+ this.toast(t2("picker.removeTicketsUntilOrFewer", { count: this.maxTickets }), "warning");
6290
6898
  return;
6291
6899
  }
6292
6900
  const committed = this.committedSelection();
@@ -6312,7 +6920,7 @@ var SeatPicker = class _SeatPicker {
6312
6920
  hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : hold;
6313
6921
  }
6314
6922
  if (!hold) {
6315
- this.toast("One or more seats were just taken. Please pick again.", "error");
6923
+ this.toast(this.tf("picker.seatsJustTaken", "One or more seats were just taken. Please pick again."), "error");
6316
6924
  this.setCtaPhase("idle");
6317
6925
  this.syncTray();
6318
6926
  return;
@@ -6329,7 +6937,7 @@ var SeatPicker = class _SeatPicker {
6329
6937
  const problem = err;
6330
6938
  const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
6331
6939
  if (problem.reason === "event_closed") this.setSalesClosed(true);
6332
- const message = problem.reason === "event_closed" ? "Seat sales have closed for this event." : labels.length ? `${labels.join(", ")} ${labels.length === 1 ? "is" : "are"} no longer available. Choose another ${labels.length === 1 ? "seat" : "group"}.` : "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.");
6333
6941
  this.toast(message, "error");
6334
6942
  this.setCtaPhase("idle");
6335
6943
  } finally {
@@ -6343,14 +6951,14 @@ var SeatPicker = class _SeatPicker {
6343
6951
  const ms = Math.max(0, this.holdExpiresAt - Date.now());
6344
6952
  const m = Math.floor(ms / 6e4);
6345
6953
  const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
6346
- return `Yours for ${m}:${s} \u2014 you won\u2019t be charged yet.`;
6954
+ return t2("picker.holdReassurance", { time: `${m}:${s}` });
6347
6955
  }
6348
6956
  startHoldTimer(expiresAt) {
6349
6957
  this.stopHoldTimer();
6350
6958
  this.holdExpiresAt = expiresAt;
6351
6959
  if (this.hold) this.rememberHold(this.hold);
6352
6960
  const pill = this.els.hold;
6353
- pill.innerHTML = '<span class="sl-hold-dot" aria-hidden="true"></span><span>Held</span><span class="sl-hold-time" data-ref="holdTime"></span>';
6961
+ pill.innerHTML = `<span class="sl-hold-dot" aria-hidden="true"></span><span>${this.tf("picker.held", "Held")}</span><span class="sl-hold-time" data-ref="holdTime"></span>`;
6354
6962
  const time = pill.querySelector('[data-ref="holdTime"]');
6355
6963
  this.els.holdNote?.classList.add("on");
6356
6964
  const tick = () => {
@@ -6383,7 +6991,7 @@ var SeatPicker = class _SeatPicker {
6383
6991
  const totalSecs = Math.ceil(ms / 1e3);
6384
6992
  const m = Math.floor(totalSecs / 60);
6385
6993
  const s = String(totalSecs % 60).padStart(2, "0");
6386
- this.els.extendTxt.innerHTML = `Your seats are held for <b>${m}:${s}</b>. Need more time?`;
6994
+ this.els.extendTxt.innerHTML = t2("picker.seatsHeldForNeedMoreTime", { time: `${m}:${s}` });
6387
6995
  this.extendEl.classList.add("on");
6388
6996
  } else {
6389
6997
  this.extendEl.classList.remove("on");
@@ -6393,7 +7001,7 @@ var SeatPicker = class _SeatPicker {
6393
7001
  const btn = this.els.extendBtn;
6394
7002
  btn.disabled = true;
6395
7003
  const prev = btn.textContent;
6396
- btn.textContent = "Adding\u2026";
7004
+ btn.textContent = this.tf("picker.addingEllipsis", "Adding\u2026");
6397
7005
  try {
6398
7006
  const h = await this.controller.extendHold(this.opts.holdTtlMs);
6399
7007
  if (h) {
@@ -6402,13 +7010,13 @@ var SeatPicker = class _SeatPicker {
6402
7010
  this.extendEl?.classList.remove("on");
6403
7011
  this.rememberHold(this.hold);
6404
7012
  this.emitHoldChange();
6405
- this.toast("More time added \u2014 your seats are still held.", "success");
7013
+ this.toast(this.tf("picker.moreTimeAdded", "More time added \u2014 your seats are still held."), "success");
6406
7014
  } else {
6407
- this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
7015
+ this.toast(this.tf("picker.couldNotAddMoreTime", "Couldn't add more time \u2014 please head to checkout now."), "warning");
6408
7016
  }
6409
7017
  } catch (err) {
6410
7018
  this.opts.onError?.(err);
6411
- this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
7019
+ this.toast(this.tf("picker.couldNotAddMoreTime", "Couldn't add more time \u2014 please head to checkout now."), "warning");
6412
7020
  } finally {
6413
7021
  btn.disabled = false;
6414
7022
  btn.textContent = prev;
@@ -6435,7 +7043,7 @@ var SeatPicker = class _SeatPicker {
6435
7043
  this.forgetHold();
6436
7044
  const n = handoff.lineItems.reduce((sum, i) => sum + i.quantity, 0);
6437
7045
  if (this.els.bookedSub) {
6438
- this.els.bookedSub.innerHTML = `<span class="sl-booked-seats">${n} ${n === 1 ? "ticket" : "tickets"}</span> confirmed. A confirmation is on its way.`;
7046
+ this.els.bookedSub.innerHTML = `<span class="sl-booked-seats">${tCount("picker.ticketsCount", n)}</span> ${this.tf("picker.confirmedAndOnWay", "confirmed. A confirmation is on its way.")}`;
6439
7047
  }
6440
7048
  this.bookedEl?.classList.add("on");
6441
7049
  this.opts.onBooked?.(handoff);
@@ -6517,7 +7125,7 @@ var SeatPicker = class _SeatPicker {
6517
7125
  ({ mountCheckout } = await loadHostedCheckout());
6518
7126
  } catch (err) {
6519
7127
  this.opts.onError?.(err);
6520
- this.toast("Checkout could not be opened. Your seats are still held \u2014 please try again.", "error");
7128
+ this.toast(this.tf("picker.checkoutCouldNotBeOpened", "Checkout could not be opened. Your seats are still held \u2014 please try again."), "error");
6521
7129
  this.setCtaPhase("idle");
6522
7130
  return;
6523
7131
  }
@@ -6632,9 +7240,9 @@ var SeatPicker = class _SeatPicker {
6632
7240
  rowTypeWord(details) {
6633
7241
  const authored = details?.displayType?.trim() || details?.rowType?.trim();
6634
7242
  if (authored) return authored;
6635
- if (details?.objectType === "table") return "Table";
6636
- if (details?.objectType === "booth") return "Booth";
6637
- return "Row";
7243
+ if (details?.objectType === "table") return this.tf("picker.table", "Table");
7244
+ if (details?.objectType === "booth") return this.tf("picker.booth", "Booth");
7245
+ return this.tf("picker.row", "Row");
6638
7246
  }
6639
7247
  rowShort(details) {
6640
7248
  const row = details?.rowLabel;
@@ -6657,14 +7265,15 @@ var SeatPicker = class _SeatPicker {
6657
7265
  const isGroupedTable = details.objectType === "table" && !!details.bookingMode;
6658
7266
  const isBooth = details.objectType === "booth";
6659
7267
  const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
6660
- 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">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">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">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">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">Seat</span><span class="sl-tip-val">${esc(details.displayLabel ?? details.label)}</span></div></div>`;
6661
- const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
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>`;
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>`;
6662
7271
  const limited = this.limitedViewLabel(details.commercial);
6663
7272
  const cxLine = limited ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u25D0</span>${esc(limited)}</div>` : "";
6664
7273
  const wheelchair = this.wheelchairProvisionLabel(details.wheelchairSpaceType);
6665
7274
  const wheelchairLine = wheelchair ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u267F</span>${esc(wheelchair)}</div>` : "";
6666
7275
  this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
6667
- 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;
6668
7277
  this.tipEl.style.display = "block";
6669
7278
  this.placeTooltip();
6670
7279
  }
@@ -6887,13 +7496,16 @@ var SeatPicker = class _SeatPicker {
6887
7496
  sourceLabel: this.tf("picker.chartDerivedModel", "Chart-derived model")
6888
7497
  };
6889
7498
  }
6890
- /** Route the module's decoupled analytics into the host callback, tagged buyer. */
6891
- emit3dAnalytics(event, props) {
7499
+ /** Route decoupled analytics into the host callback, tagged buyer. */
7500
+ emitAnalytics(event, props) {
6892
7501
  try {
6893
7502
  this.opts.onAnalytics?.(event, { ...props, surface: "buyer" });
6894
7503
  } catch {
6895
7504
  }
6896
7505
  }
7506
+ emit3dAnalytics(event, props) {
7507
+ this.emitAnalytics(event, props);
7508
+ }
6897
7509
  /** A 3D seat tap runs the SAME selection path as a 2D tap: toggle through the
6898
7510
  * controller, then raise the shared confirm card (bottom-sheeted in 3D). */
6899
7511
  onView3dSeatPick(seatId) {
@@ -6912,9 +7524,9 @@ var SeatPicker = class _SeatPicker {
6912
7524
  this.syncSelectionTo3d();
6913
7525
  return;
6914
7526
  }
6915
- const visualState = this.seatState3dFor(seat);
6916
- if (visualState !== "available") {
6917
- 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);
6918
7530
  this.syncSelectionTo3d();
6919
7531
  return;
6920
7532
  }
@@ -6936,31 +7548,28 @@ var SeatPicker = class _SeatPicker {
6936
7548
  else this.syncTray();
6937
7549
  this.syncSelectionTo3d();
6938
7550
  }
6939
- /** Explain a visible-but-unselectable 3D seat without entering the booking
6940
- * flow. Category colour remains visible in the venue; this card names the
6941
- * availability state explicitly so yellow never has to carry both meanings. */
6942
- 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) {
6943
7556
  const overlay = this.view3dEl;
6944
7557
  if (!overlay) return;
6945
7558
  this.dismissUnavailable3dSeat();
6946
- const status = this.controller.getStatus(seat.id);
6947
7559
  const details = this.controller.seatDetails(seat.id);
6948
7560
  const identity = details?.displayLabel ?? seat.displayLabel ?? seat.label;
6949
7561
  const section = details?.sectionLabel;
6950
7562
  const row = this.rowShort(details);
6951
- const location2 = [section, row ? `Row ${row}` : null, identity].filter(Boolean).join(" \xB7 ");
7563
+ const location2 = [section, row ? t2("picker.rowLabel", { label: row }) : null, identity].filter(Boolean).join(" \xB7 ");
6952
7564
  const copy = status === "held" ? {
6953
7565
  title: this.tf("picker.temporarilyHeld", "Temporarily held"),
6954
7566
  message: this.tf("picker.heldSeatExplanation", "Another buyer is holding this seat. It may become available again.")
6955
7567
  } : status === "booked" ? {
6956
7568
  title: this.tf("picker.sold", "Sold"),
6957
7569
  message: this.tf("picker.soldSeatExplanation", "This seat has already been booked.")
6958
- } : status === "not_for_sale" ? {
7570
+ } : {
6959
7571
  title: this.tf("picker.notForSale", "Not for sale"),
6960
7572
  message: this.tf("picker.notForSaleExplanation", "This seat is not included in the current sale.")
6961
- } : {
6962
- title: this.tf("picker.filteredSeat", "Unavailable with current filters"),
6963
- message: this.tf("picker.filteredSeatExplanation", "Change the active price or view filters to make this seat selectable.")
6964
7573
  };
6965
7574
  const card = document.createElement("div");
6966
7575
  card.className = "sl-view3d-unavailable";
@@ -7044,7 +7653,7 @@ var SeatPicker = class _SeatPicker {
7044
7653
  chip = document.createElement("div");
7045
7654
  chip.className = "sl-view3d-compare-saved";
7046
7655
  chip.setAttribute("role", "group");
7047
- chip.setAttribute("aria-label", "Saved seat comparison");
7656
+ chip.setAttribute("aria-label", this.tf("picker.savedSeatComparison", "Saved seat comparison"));
7048
7657
  const main2 = document.createElement("button");
7049
7658
  main2.type = "button";
7050
7659
  main2.className = "main";
@@ -7056,7 +7665,7 @@ var SeatPicker = class _SeatPicker {
7056
7665
  clear.type = "button";
7057
7666
  clear.className = "clear";
7058
7667
  clear.textContent = "\xD7";
7059
- clear.setAttribute("aria-label", "Clear saved seat comparison");
7668
+ clear.setAttribute("aria-label", this.tf("picker.clearSavedSeatComparison", "Clear saved seat comparison"));
7060
7669
  clear.addEventListener("click", () => this.clearView3dComparison());
7061
7670
  chip.append(main2, clear);
7062
7671
  overlay.appendChild(chip);
@@ -7065,8 +7674,8 @@ var SeatPicker = class _SeatPicker {
7065
7674
  const count = this.view3dCompareSeatIds.length;
7066
7675
  const main = chip.querySelector(".main");
7067
7676
  if (main) {
7068
- main.textContent = count > 1 ? `Compare ${count}` : "1 seat saved";
7069
- main.setAttribute("aria-label", count > 1 ? `Open comparison of ${count} seats` : "One seat saved; choose another to compare");
7677
+ main.textContent = count > 1 ? t2("picker.compareCount", { count }) : this.tf("picker.oneSeatSaved", "1 seat saved");
7678
+ main.setAttribute("aria-label", count > 1 ? t2("picker.openComparisonOfSeats", { count }) : this.tf("picker.oneSeatSavedChooseAnother", "One seat saved; choose another to compare"));
7070
7679
  }
7071
7680
  }
7072
7681
  view3dComparisonSnapshot(seatId) {
@@ -7338,13 +7947,12 @@ var SeatPicker = class _SeatPicker {
7338
7947
  const floors = handle.floors();
7339
7948
  const floorLabels = new Set(floors.map((floor) => floor.label.trim().toLocaleLowerCase()));
7340
7949
  const zones = handle.zones().filter((zone) => zone.seatCount > 0 && !floorLabels.has(zone.label.trim().toLocaleLowerCase()));
7341
- 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()));
7342
7951
  const sections = zones.length > 1 ? [] : allSections;
7343
7952
  const wantFloors = floors.length > 1;
7344
7953
  const wantZones = zones.length > 1;
7345
7954
  const wantSections = sections.length > 1;
7346
- const wantLocator = allSections.length > 0 && handle.rows().length > 0;
7347
- if (!wantFloors && !wantZones && !wantSections && !wantLocator) return;
7955
+ if (!wantFloors && !wantZones && !wantSections) return;
7348
7956
  const nav = document.createElement("div");
7349
7957
  nav.className = "sl-view3d-nav";
7350
7958
  const finderToggle = document.createElement("button");
@@ -7354,7 +7962,7 @@ var SeatPicker = class _SeatPicker {
7354
7962
  const setFinderOpen = (open) => {
7355
7963
  nav.classList.toggle("is-open", open);
7356
7964
  finderToggle.setAttribute("aria-expanded", String(open));
7357
- finderToggle.textContent = open ? "Close seat finder" : "Find a seat";
7965
+ finderToggle.textContent = open ? this.tf("picker.closeVenueNav", "Close") : wantFloors && (wantZones || wantSections) ? this.tf("picker.levelsAndAreas", "Levels & areas") : wantFloors ? this.tf("picker.levels", "Levels") : this.tf("picker.areas", "Areas");
7358
7966
  };
7359
7967
  finderToggle.addEventListener("click", () => setFinderOpen(!nav.classList.contains("is-open")));
7360
7968
  nav.addEventListener("keydown", (event) => {
@@ -7366,16 +7974,24 @@ var SeatPicker = class _SeatPicker {
7366
7974
  });
7367
7975
  setFinderOpen(false);
7368
7976
  nav.appendChild(finderToggle);
7977
+ let syncAreasForFloor = (_index) => void 0;
7978
+ let syncPressedFloor = (_index) => void 0;
7369
7979
  if (wantFloors) {
7370
7980
  const row = document.createElement("div");
7371
7981
  row.setAttribute("role", "group");
7372
7982
  row.setAttribute("aria-label", this.tf("picker.levels", "Levels"));
7373
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
+ };
7374
7991
  const select = (index) => {
7375
7992
  if (!handle.focusFloor(index)) return;
7376
- pills.forEach((p) => {
7377
- p.setAttribute("aria-pressed", String((p.dataset.floor === "" ? null : Number(p.dataset.floor)) === index));
7378
- });
7993
+ syncPressedFloor(index);
7994
+ syncAreasForFloor(index);
7379
7995
  };
7380
7996
  const add = (label, index) => {
7381
7997
  const b = document.createElement("button");
@@ -7391,7 +8007,7 @@ var SeatPicker = class _SeatPicker {
7391
8007
  row.appendChild(b);
7392
8008
  };
7393
8009
  add(this.tf("picker.allLevels", "All levels"), null);
7394
- for (const f of floors) add(f.label || `Level ${f.index + 1}`, f.index);
8010
+ for (const f of floors) add(f.label || t2("picker.levelNumber", { number: f.index + 1 }), f.index);
7395
8011
  nav.appendChild(row);
7396
8012
  }
7397
8013
  if (wantZones || wantSections) {
@@ -7400,119 +8016,55 @@ var SeatPicker = class _SeatPicker {
7400
8016
  row.setAttribute("aria-label", this.tf("picker.areas", "Areas"));
7401
8017
  const entries = wantZones ? zones.map((z) => ({ id: z.id, label: z.label || z.id, go: () => {
7402
8018
  handle.focusZone(z.id);
7403
- } })) : sections.map((s) => ({ id: s.id, label: s.label || s.id, go: () => {
7404
- handle.focusSection(s.id);
7405
- } }));
7406
- if (!wantZones && entries.length > MAX_3D_SECTION_PILLS) {
7407
- const select = document.createElement("select");
7408
- select.setAttribute("aria-label", this.tf("picker.jumpToSection", "Jump to section"));
7409
- const placeholder = document.createElement("option");
7410
- placeholder.value = "";
7411
- placeholder.textContent = this.tf("picker.jumpToSection", "Jump to section");
7412
- select.appendChild(placeholder);
7413
- for (const entry of entries) {
7414
- const option = document.createElement("option");
7415
- option.value = entry.id;
7416
- option.textContent = entry.label;
7417
- select.appendChild(option);
7418
- }
7419
- select.addEventListener("change", () => {
7420
- entries.find((entry) => entry.id === select.value)?.go();
7421
- });
7422
- row.appendChild(select);
7423
- } else {
7424
- for (const e of entries) {
7425
- const b = document.createElement("button");
7426
- b.type = "button";
7427
- b.textContent = e.label;
7428
- b.addEventListener("click", () => {
7429
- e.go();
7430
- setFinderOpen(false);
7431
- });
7432
- row.appendChild(b);
7433
- }
7434
- }
7435
- nav.appendChild(row);
7436
- }
7437
- if (wantLocator) {
7438
- const locator = document.createElement("div");
7439
- locator.className = "sl-view3d-locator";
7440
- locator.setAttribute("role", "group");
7441
- locator.setAttribute("aria-label", "Find an exact seat in 3D");
7442
- const sectionSelect = document.createElement("select");
7443
- sectionSelect.setAttribute("aria-label", "Choose section in 3D");
7444
- const rowSelect = document.createElement("select");
7445
- rowSelect.setAttribute("aria-label", "Choose row in 3D");
7446
- const seatSelect = document.createElement("select");
7447
- seatSelect.setAttribute("aria-label", "Choose seat in 3D");
7448
- const view = document.createElement("button");
7449
- view.type = "button";
7450
- view.textContent = "Inspect seat";
7451
- view.disabled = true;
7452
- const fill = (select, placeholder, entries) => {
7453
- select.replaceChildren();
7454
- const first = document.createElement("option");
7455
- first.value = "";
7456
- first.textContent = placeholder;
7457
- select.appendChild(first);
7458
- for (const entry of entries) {
7459
- const option = document.createElement("option");
7460
- option.value = entry.id;
7461
- option.textContent = entry.label;
7462
- 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);
7463
8026
  }
7464
- select.value = "";
7465
- };
7466
- fill(sectionSelect, "1. Section", allSections.map((section) => ({
7467
- id: section.id,
7468
- label: `${section.label} \xB7 ${section.seatCount.toLocaleString()} seats`
7469
- })));
7470
- fill(rowSelect, "2. Row", []);
7471
- fill(seatSelect, "3. Seat", []);
7472
- rowSelect.disabled = true;
7473
- seatSelect.disabled = true;
7474
- sectionSelect.addEventListener("change", () => {
7475
- const sectionId = sectionSelect.value;
7476
- view.disabled = true;
7477
- fill(seatSelect, "3. Seat", []);
7478
- seatSelect.disabled = true;
7479
- if (!sectionId || !handle.focusSection(sectionId)) {
7480
- fill(rowSelect, "2. Row", []);
7481
- rowSelect.disabled = true;
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();
7482
8033
  return;
7483
8034
  }
7484
- const rows = handle.rows(sectionId);
7485
- fill(rowSelect, "2. Row", rows.map((row) => ({
7486
- id: row.id,
7487
- label: `${row.label} \xB7 ${row.seatCount} seats`
7488
- })));
7489
- rowSelect.disabled = rows.length === 0;
7490
- });
7491
- rowSelect.addEventListener("change", () => {
7492
- const rowId = rowSelect.value;
7493
- view.disabled = true;
7494
- if (!rowId || !handle.focusRow(rowId)) {
7495
- fill(seatSelect, "3. Seat", []);
7496
- seatSelect.disabled = true;
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);
7497
8053
  return;
7498
8054
  }
7499
- const seats = handle.seatsInRow(rowId);
7500
- fill(seatSelect, "3. Seat", seats);
7501
- seatSelect.disabled = seats.length === 0;
7502
- });
7503
- seatSelect.addEventListener("change", () => {
7504
- const seatId = seatSelect.value;
7505
- view.disabled = !seatId;
7506
- });
7507
- view.addEventListener("click", () => {
7508
- const seatId = seatSelect.value;
7509
- if (seatId) {
7510
- setFinderOpen(false);
7511
- this.onView3dSeatPick(seatId);
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();
8061
+ setFinderOpen(false);
8062
+ });
8063
+ row.appendChild(button);
7512
8064
  }
7513
- });
7514
- locator.append(sectionSelect, rowSelect, seatSelect, view);
7515
- nav.appendChild(locator);
8065
+ };
8066
+ syncAreasForFloor = renderEntries;
8067
+ renderEntries(null);
7516
8068
  }
7517
8069
  overlay.appendChild(nav);
7518
8070
  }
@@ -7529,7 +8081,7 @@ var SeatPicker = class _SeatPicker {
7529
8081
  const overlay = document.createElement("div");
7530
8082
  overlay.className = "sl-view3d";
7531
8083
  overlay.setAttribute("role", "group");
7532
- overlay.setAttribute("aria-label", "Interactive 3D venue view");
8084
+ overlay.setAttribute("aria-label", this.tf("picker.interactive3dVenueView", "Interactive 3D venue view"));
7533
8085
  const back = document.createElement("button");
7534
8086
  back.type = "button";
7535
8087
  back.className = "sl-view3d-back";
@@ -7555,7 +8107,7 @@ var SeatPicker = class _SeatPicker {
7555
8107
  fullscreen.type = "button";
7556
8108
  fullscreen.className = "sl-view3d-fs";
7557
8109
  fullscreen.textContent = "\u26F6";
7558
- fullscreen.setAttribute("aria-label", "Full screen");
8110
+ fullscreen.setAttribute("aria-label", this.tf("picker.fullScreen", "Full screen"));
7559
8111
  fullscreen.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback || this.framedFs));
7560
8112
  fullscreen.addEventListener("click", () => this.toggleFullscreen());
7561
8113
  overlay.appendChild(fullscreen);
@@ -7592,7 +8144,7 @@ var SeatPicker = class _SeatPicker {
7592
8144
  onSeatInspect: (id) => this.onView3dSeatPick(id),
7593
8145
  onSectionFocusChange: (sectionId) => {
7594
8146
  const select = overlay.querySelector(
7595
- 'select[aria-label="Choose section in 3D"]'
8147
+ 'select[data-locator="section"]'
7596
8148
  );
7597
8149
  const next = sectionId ?? "";
7598
8150
  if (!select || select.value === next) return;
@@ -7689,7 +8241,7 @@ var SeatPicker = class _SeatPicker {
7689
8241
  if (button) {
7690
8242
  button.disabled = true;
7691
8243
  button.classList.add("sl-busy");
7692
- button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
8244
+ button.innerHTML = `<span class="sl-ba-spin" aria-hidden="true"></span>${this.tf("picker.findingEllipsis", "Finding\u2026")}`;
7693
8245
  }
7694
8246
  try {
7695
8247
  const h = await this.controller.bestAvailable(qty, categoryKey, { ...opts, ttlMs: this.opts.holdTtlMs });
@@ -7711,7 +8263,7 @@ var SeatPicker = class _SeatPicker {
7711
8263
  } catch (err) {
7712
8264
  this.opts.onError?.(err);
7713
8265
  const reason = err?.reason;
7714
- const message = reason === "not_enough_together" ? `We couldn't find ${qty} seats together. Try fewer seats or another ticket type.` : reason === "sold_out" ? "That ticket type is sold out. Try another ticket type." : reason === "event_closed" ? "Seat sales have closed for this event." : "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.");
7715
8267
  this.toast(message, "error");
7716
8268
  return null;
7717
8269
  } finally {
@@ -7740,7 +8292,7 @@ var SeatPicker = class _SeatPicker {
7740
8292
  }
7741
8293
  }
7742
8294
  if (!released) {
7743
- this.toast("Couldn't release your tickets. Your hold is unchanged.", "error");
8295
+ this.toast(this.tf("picker.couldNotReleaseTickets", "Couldn't release your tickets. Your hold is unchanged."), "error");
7744
8296
  return;
7745
8297
  }
7746
8298
  this.hold = null;