@seatlayer/js 0.48.2 → 0.50.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
@@ -1,38 +1,3 @@
1
- import {
2
- SeatManager
3
- } from "./chunk-SD6GL3DT.js";
4
- import {
5
- ACCESS_LINK_DEFAULTS,
6
- ChannelsMode,
7
- PUBLIC_CHANNEL_ID,
8
- PUBLIC_CHANNEL_NAME,
9
- accessIntentDescription,
10
- accessIntentLabel,
11
- accessLine,
12
- accessLinkBadge,
13
- accessLinkErrorCopy,
14
- accessLinkIsLive,
15
- accessLinkPolicyLines,
16
- bucketRows,
17
- bucketRowsHtml,
18
- dropReviewRows,
19
- intentForbidsCopy,
20
- intentSwitchBlockedCopy,
21
- isPublicChannelId,
22
- markerLetter,
23
- markerOf,
24
- mutationCount,
25
- needsMoveConfirmation,
26
- planAssignment,
27
- retryAfterCopy,
28
- selectionSources,
29
- stateBadge,
30
- suggestMarker
31
- } from "./chunk-KNMZQZXR.js";
32
- import {
33
- ManageApi,
34
- ManageApiError
35
- } from "./chunk-H4OJF6LE.js";
36
1
  import {
37
2
  __privateAdd,
38
3
  __privateGet,
@@ -1448,6 +1413,12 @@ var EmbeddedDesigner = class {
1448
1413
  */
1449
1414
  this.autoRecoverUsed = false;
1450
1415
  this.phase = "loading";
1416
+ /**
1417
+ * Set only after an identity-checked `ready`. Unlike `phase`, this remains true
1418
+ * if a later fatal error renders the error card, so no subsequent callback can
1419
+ * shed the chart/workspace identity the live session already established.
1420
+ */
1421
+ this.identityEstablished = false;
1451
1422
  this.restoreContainerPosition = null;
1452
1423
  // Host-side fullscreen pin: saved state we restore on `off`/Escape/destroy.
1453
1424
  this.pinned = false;
@@ -1514,14 +1485,21 @@ var EmbeddedDesigner = class {
1514
1485
  expiresAt: typeof data.expiresAt === "number" ? data.expiresAt : void 0,
1515
1486
  code: typeof data.code === "string" ? data.code : void 0,
1516
1487
  message: typeof data.message === "string" ? data.message : void 0,
1517
- meta: data.meta
1488
+ meta: data.meta,
1489
+ fatal: typeof data.fatal === "boolean" ? data.fatal : void 0,
1490
+ action: typeof data.action === "string" ? data.action : void 0
1518
1491
  };
1519
- if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId || this.options.expectedWorkspaceId && message.workspaceId && message.workspaceId !== this.options.expectedWorkspaceId) {
1492
+ const identityRequired = this.identityEstablished || message.type === "seatlayer.designer.ready" || message.type === "seatlayer.designer.saved" || message.type === "seatlayer.designer.published" || message.type === "seatlayer.designer.close";
1493
+ const chartMismatch = this.options.expectedChartId !== void 0 && message.chartId !== this.options.expectedChartId && (identityRequired || message.chartId !== void 0);
1494
+ const workspaceMismatch = this.options.expectedWorkspaceId !== void 0 && message.workspaceId !== this.options.expectedWorkspaceId && (identityRequired || message.workspaceId !== void 0);
1495
+ if (chartMismatch || workspaceMismatch) {
1520
1496
  this.showError("mismatch");
1521
1497
  return;
1522
1498
  }
1523
1499
  switch (message.type) {
1524
1500
  case "seatlayer.designer.ready":
1501
+ this.identityEstablished = true;
1502
+ this.sessionExpiresAt = message.expiresAt;
1525
1503
  this.phase = "ready";
1526
1504
  this.clearTimeoutTimer();
1527
1505
  this.removeOverlay();
@@ -1603,6 +1581,25 @@ var EmbeddedDesigner = class {
1603
1581
  getIframe() {
1604
1582
  return this.frame;
1605
1583
  }
1584
+ /** Update iframe sizing without replacing the live Designer session. */
1585
+ setSizing(height, minHeight) {
1586
+ this.options = { ...this.options, height, minHeight };
1587
+ if (!this.frame) return;
1588
+ this.stopFill();
1589
+ this.lastAutoHeight = "";
1590
+ if (this.fillEnabled()) {
1591
+ if (!this.pinned) this.setFrameHeight("100%");
1592
+ this.startFill();
1593
+ } else if (!this.pinned) {
1594
+ this.setFrameHeight(`${height}px`);
1595
+ }
1596
+ }
1597
+ /** Update renewal/expiry-recovery policy without replacing the iframe. */
1598
+ setRelaunchPolicy(onRequestRelaunch, autoRenewSession) {
1599
+ this.options = { ...this.options, onRequestRelaunch, autoRenewSession };
1600
+ this.clearRenewTimer();
1601
+ if (this.phase === "ready") this.scheduleRenewal(this.sessionExpiresAt);
1602
+ }
1606
1603
  destroy() {
1607
1604
  window.removeEventListener("message", this.handleMessage);
1608
1605
  this.stopFill();
@@ -1617,6 +1614,8 @@ var EmbeddedDesigner = class {
1617
1614
  this.fillMode = null;
1618
1615
  this.designerOrigin = "";
1619
1616
  this.phase = "loading";
1617
+ this.identityEstablished = false;
1618
+ this.sessionExpiresAt = void 0;
1620
1619
  this.lastAutoHeight = "";
1621
1620
  }
1622
1621
  loadingStateEnabled() {
@@ -1767,6 +1766,7 @@ var EmbeddedDesigner = class {
1767
1766
  else this.frame.setAttribute("style", this.frameStyleBeforeFs);
1768
1767
  if (this.fillEnabled()) this.applyFill();
1769
1768
  else if (this.autoResizeEnabled() && this.lastAutoHeight) this.setFrameHeight(this.lastAutoHeight);
1769
+ else if (typeof this.options.height === "number") this.setFrameHeight(`${this.options.height}px`);
1770
1770
  }
1771
1771
  this.frameStyleBeforeFs = null;
1772
1772
  if (this.docOverflowBeforeFs !== null) {
@@ -2031,6 +2031,7 @@ import {
2031
2031
  tCount
2032
2032
  } from "@seatlayer/core";
2033
2033
  import { isAuthoredSeatView, seatViewDisclosure } from "@seatlayer/core/view3d/crossfade/panorama";
2034
+ import { seatConfidenceDisclosure } from "@seatlayer/core/core/seatConfidence";
2034
2035
  import {
2035
2036
  browserPanoramaConstraints,
2036
2037
  loadPanoramaImage,
@@ -2328,7 +2329,7 @@ async function loadHostedCheckout() {
2328
2329
  cdnChunkUrl("seatlayer-checkout.mjs")
2329
2330
  );
2330
2331
  }
2331
- return import("./hostedCheckout-F6C6VCCG.js");
2332
+ return import("./hostedCheckout-GVS6UKYA.js");
2332
2333
  }
2333
2334
  function paymentsOffReason(reason) {
2334
2335
  return reason === "unavailable_for_event" || reason === "payments_off_for_event" ? reason : "not_configured";
@@ -2344,7 +2345,12 @@ var CSS = (
2344
2345
  never inherits (or fights) the host page's own timing. Values mirror
2345
2346
  docs/motion-system-2026-08-01.md \xA72. */
2346
2347
  --slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;
2347
- --slm-mo-out:cubic-bezier(0.2,0.8,0.2,1);--slm-mo-exit:cubic-bezier(0.4,0,1,1)}
2348
+ --slm-mo-out:cubic-bezier(0.2,0.8,0.2,1);--slm-mo-exit:cubic-bezier(0.4,0,1,1);
2349
+ /* Semantic palette tokens, defined here (not resolveTokens) because they are
2350
+ the WIDGET's meanings, not an org's brand: premium gold, environment
2351
+ warning, danger, success. Hosts can still override them with CSS. */
2352
+ --sl-premium:#e8c15a;--sl-premium-deep:#c9a24b;--sl-premium-ink:#1c1608;
2353
+ --sl-warn:#d8a425;--sl-danger:#e5484d;--sl-success:#22a06b}
2348
2354
  .sl-picker *{box-sizing:border-box;margin:0;padding:0}
2349
2355
  .sl-picker button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
2350
2356
 
@@ -2370,21 +2376,41 @@ var CSS = (
2370
2376
  .sl-close.on{display:inline-flex}
2371
2377
  .sl-close svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round}
2372
2378
 
2373
- /* A page or popup may already own the event heading. In that case only the
2374
- duplicate identity disappears; operational header controls stay available.
2375
- Zero block padding lets an otherwise-empty header collapse completely. */
2376
- .sl-picker[data-event-details-hidden="true"] .sl-head{padding-block:0;border-bottom-width:0}
2377
- .sl-picker[data-event-details-hidden="true"] .sl-logo,
2378
- .sl-picker[data-event-details-hidden="true"] .sl-head-info{display:none!important}
2379
- .sl-picker[data-event-details-hidden="true"] .sl-hold-pill.on,
2380
- .sl-picker[data-event-details-hidden="true"] .sl-closed-pill.on,
2381
- .sl-picker[data-event-details-hidden="true"] .sl-close.on{margin-block:10px}
2379
+ /* A page or popup may already own the event heading \u2014 but "already" is true at
2380
+ load and false thirty seconds later: the buyer scrolls, the sheet opens over
2381
+ the map, fullscreen removes the page, and two-tab comparison shopping is
2382
+ normal ticket behaviour. So identity DEMOTES instead of disappearing: one
2383
+ compact line (thumb + name) that answers "which event is this money for"
2384
+ without competing with the host's own heading. The hold clock keeps a
2385
+ labelled row around it instead of floating in a dark corner. */
2386
+ .sl-picker[data-event-details-hidden="true"] .sl-head{padding-block:7px}
2387
+ .sl-picker[data-event-details-hidden="true"] .sl-logo{width:22px;height:22px;border-radius:6px;font-size:11px}
2388
+ .sl-picker[data-event-details-hidden="true"] .sl-head-name{font-size:12.5px}
2389
+ .sl-picker[data-event-details-hidden="true"] .sl-head-meta{display:none}
2382
2390
 
2383
2391
  /* body */
2384
2392
  .sl-body{display:flex;flex:1;min-height:0}
2385
2393
  .sl-map{position:relative;flex:1;min-width:0}
2386
2394
  .sl-map-host{position:absolute;inset:0}
2387
2395
  .sl-side{width:300px;flex:none;border-left:1px solid var(--sl-line);display:flex;flex-direction:column;min-height:0;overflow:hidden}
2396
+ /* Wide-layout panel collapse: THE CHART IS THE PRODUCT, and a fixed 300px
2397
+ panel holding an empty cart is the map's room spent on nothing. The panel
2398
+ slides to zero (children keep their laid-out width so text doesn't rewrap
2399
+ mid-slide); the "Tickets" pill in the top-right region brings it back, and
2400
+ the first seat landing in the cart reopens it automatically \u2014 a collapsed
2401
+ panel must never hide a checkout. */
2402
+ .sl-picker[data-layout="wide"] .sl-side{transition:width var(--slm-mo-slow) var(--slm-mo-out)}
2403
+ .sl-picker[data-layout="wide"][data-side-collapsed="true"] .sl-side{width:0;min-width:0;border-left-width:0}
2404
+ /* min-width:0 above is load-bearing: the panel is a flex item, and its
2405
+ automatic minimum size (from these 300px-wide children) would otherwise
2406
+ clamp the explicit width:0 straight back to 300 \u2014 verified in-browser. */
2407
+ .sl-picker[data-layout="wide"][data-side-collapsed="true"] .sl-side>*{min-width:300px}
2408
+ .sl-side-toggle{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:36px;padding:0 12px;
2409
+ border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.02em;white-space:nowrap;
2410
+ background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-text);transition:border-color .15s}
2411
+ .sl-side-toggle:hover{border-color:var(--sl-muted)}
2412
+ .sl-side-toggle svg{width:13px;height:13px;stroke:currentColor;stroke-width:2.2;fill:none;stroke-linecap:round;stroke-linejoin:round}
2413
+ .sl-picker[data-layout="narrow"] .sl-side-toggle{display:none}
2388
2414
 
2389
2415
  /* narrow (container < 640px): map-first \u2014 the map claims ~80-85% of the
2390
2416
  container and the side panel becomes a PEEKING bottom sheet (AXS/Ticketmaster
@@ -2467,6 +2493,15 @@ var CSS = (
2467
2493
  the buyer explicitly opens the ticket panel. */
2468
2494
  .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-filtersec.has{display:block!important}
2469
2495
  .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-filters.has{display:flex!important}
2496
+ /* The price list is the mobile buyer's ONLY legend \u2014 colour \u2192 ticket type \u2192
2497
+ price \u2192 how many are left. Hiding it in the peek state is right (map-first);
2498
+ never restoring it forced phone buyers to tap dots to learn prices. The OPEN
2499
+ sheet brings it back, exactly like the filters row above. The tray hint comes
2500
+ back with it: the first-time buyer on a phone needs the instruction more than
2501
+ 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}
2504
+ .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-tray-hint{display:block!important}
2470
2505
  .sl-cbbtn{width:32px;height:32px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
2471
2506
  color:var(--sl-text);display:flex;align-items:center;justify-content:center;transition:border-color .15s}
2472
2507
  .sl-cbbtn:hover{border-color:var(--sl-muted)}
@@ -2476,13 +2511,13 @@ var CSS = (
2476
2511
  .sl-offer{display:none;margin:12px 14px 2px;padding:12px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));
2477
2512
  border-radius:12px;background:color-mix(in srgb,var(--sl-accent) 8%,var(--sl-surface));color:var(--sl-text)}
2478
2513
  .sl-offer.has{display:block}.sl-offer-main{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}
2479
- .sl-offer-copy{min-width:0}.sl-offer-kicker{display:block;font-size:9px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-accent)}
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)}
2480
2515
  .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)}
2481
2516
  .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;
2482
2517
  display:grid;place-items:center;cursor:pointer;font-size:12px;font-weight:850;color:var(--sl-text);background:var(--sl-surface)}
2483
2518
  .sl-offer-info>summary::-webkit-details-marker{display:none}.sl-offer-info[open]>summary{border-color:var(--sl-accent);color:var(--sl-accent)}
2484
2519
  .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)}
2485
- .sl-sec{padding:14px 14px 4px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:700}
2520
+ .sl-sec{padding:14px 14px 4px;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:700}
2486
2521
  .sl-prices-sec{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:13px}
2487
2522
  .sl-price-select{min-height:32px;max-width:130px;padding:5px 28px 5px 9px;border:1px solid var(--sl-line);border-radius:9px;
2488
2523
  background:var(--sl-surface);color:var(--sl-text);font:inherit;font-size:11px;font-weight:750;letter-spacing:0;text-transform:none}
@@ -2492,7 +2527,7 @@ var CSS = (
2492
2527
  padding:0 6px;margin:0 -6px;border-radius:8px;cursor:pointer;transition:background .15s}
2493
2528
  .sl-price-row:hover,.sl-price-row:focus-visible{background:color-mix(in srgb,var(--sl-line) 40%,transparent)}
2494
2529
  .sl-price-row.sl-active{background:color-mix(in srgb,var(--sl-accent) 9%,transparent)}
2495
- .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:9px;font-weight:750}
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}
2496
2531
  .sl-price-row.sl-active .sl-price-label{color:var(--sl-accent)}
2497
2532
  .sl-dot{width:9px;height:9px;border-radius:50%;flex:none}
2498
2533
  .sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
@@ -2535,7 +2570,7 @@ var CSS = (
2535
2570
  .sl-chip-id .fld{min-width:0}
2536
2571
  .sl-chip-id .fld.sec{flex:1}
2537
2572
  .sl-chip-id .fld.mid{flex:none;text-align:center}
2538
- .sl-chip-eb{display:block;font-size:8px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);margin-bottom:1px}
2573
+ .sl-chip-eb{display:block;font-size:10px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);margin-bottom:1px}
2539
2574
  .sl-chip-id .val{display:block;font-weight:600;font-size:13px;line-height:1.25;white-space:nowrap}
2540
2575
  .sl-chip-id .fld.sec .val{overflow:hidden;text-overflow:ellipsis}
2541
2576
  .sl-chip-sub{display:flex;align-items:center;gap:6px;min-width:0}
@@ -2545,7 +2580,7 @@ var CSS = (
2545
2580
  .sl-chip .rm,.sl-chip .view{flex:1;min-height:26px;border-radius:0;display:flex;align-items:center;justify-content:center;
2546
2581
  color:var(--sl-muted);transition:color .15s,background .15s}
2547
2582
  .sl-chip .view{border-top:1px solid var(--sl-line)}
2548
- .sl-chip .rm:hover,.sl-chip .rm:focus-visible{color:#e5484d;background:color-mix(in srgb,#e5484d 9%,transparent)}
2583
+ .sl-chip .rm:hover,.sl-chip .rm:focus-visible{color:var(--sl-danger);background:color-mix(in srgb,var(--sl-danger) 9%,transparent)}
2549
2584
  .sl-chip .view:hover,.sl-chip .view:focus-visible{color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
2550
2585
  .sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
2551
2586
  .sl-chip .view svg{width:13px;height:13px;stroke:currentColor;stroke-width:1.8;fill:none}
@@ -2555,7 +2590,7 @@ var CSS = (
2555
2590
  .sl-live{display:none;align-items:center;gap:7px;margin:10px 14px 0;padding:7px 9px;flex:none;
2556
2591
  border:1px solid var(--sl-line);border-radius:8px;background:color-mix(in srgb,var(--sl-accent) 4%,var(--sl-surface));
2557
2592
  font-size:11px;color:var(--sl-muted)}
2558
- .sl-live .dot{width:6px;height:6px;border-radius:999px;background:#22a06b;box-shadow:0 0 6px rgba(34,160,107,.75);flex:none}
2593
+ .sl-live .dot{width:6px;height:6px;border-radius:999px;background:var(--sl-success);box-shadow:0 0 6px color-mix(in srgb,var(--sl-success) 75%,transparent);flex:none}
2559
2594
  .sl-live span:last-child{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2560
2595
  .sl-live.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
2561
2596
 
@@ -2625,17 +2660,17 @@ var CSS = (
2625
2660
  .sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-left"]{max-width:30%}
2626
2661
  .sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-center"]{max-width:44%}
2627
2662
 
2628
- /* TEST MODE is environment context, not an action. A clipped corner ribbon
2629
- keeps it persistent without impersonating a button or competing with Map/3D.
2630
- The top-left interactive region moves below it only on test events. */
2631
- .sl-testbadge{position:absolute;top:17px;left:-38px;z-index:6;width:142px;padding:5px 0;
2632
- transform:rotate(-45deg);text-align:center;pointer-events:none;
2633
- font-size:9.5px;font-weight:850;letter-spacing:.13em;line-height:1.2;text-transform:uppercase;
2634
- white-space:nowrap;background:var(--sl-accent);color:var(--sl-accent-ink);
2635
- box-shadow:0 2px 8px rgba(0,0,0,.28)}
2636
- .sl-picker[data-event-mode="test"] .sl-anchor[data-region="top-left"]{top:96px}
2637
- .sl-picker[data-layout="narrow"] .sl-testbadge{top:14px;left:-35px;width:128px;font-size:8.5px}
2638
- .sl-picker[data-layout="narrow"][data-event-mode="test"] .sl-anchor[data-region="top-left"]{top:88px}
2663
+ /* TEST MODE is environment context, not an action. A small squared chip that
2664
+ flows FIRST in the top-left anchor region: the old rotated corner ribbon sat
2665
+ at a negative offset and was clipped by the widget root's rounded
2666
+ overflow:hidden (its ends cut mid-word), and its accent fill made an
2667
+ environment flag wear the same gold as "buy". Amber warning tone, dashed
2668
+ border \u2014 reads as a tag, never as a control; the a11y chips wrap beside it
2669
+ in the same region, so no displacement hack is needed. */
2670
+ .sl-testbadge{display:inline-flex;align-items:center;padding:5px 10px;border-radius:7px;pointer-events:none;
2671
+ 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));
2673
+ border:1px dashed color-mix(in srgb,var(--sl-warn) 55%,transparent)}
2639
2674
 
2640
2675
  /* zoom column (flows within the bottom-right region) */
2641
2676
  .sl-zoom{display:flex;flex-direction:column;gap:6px}
@@ -2645,6 +2680,18 @@ var CSS = (
2645
2680
  color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
2646
2681
  .sl-zoom button:hover{border-color:var(--sl-muted)}
2647
2682
  .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. */
2688
+ .sl-zfs-lbl{display:none}
2689
+ .sl-fs-pill.sl-fs-pill{display:inline-flex;align-items:center;justify-content:center;width:auto;min-height:40px;height:auto;
2690
+ padding:0 13px;gap:6px;border-radius:999px;font-size:11px;font-weight:800;
2691
+ background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-text);transition:border-color .15s}
2692
+ .sl-fs-pill.sl-fs-pill:hover{border-color:var(--sl-muted)}
2693
+ .sl-fs-pill svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
2694
+ .sl-fs-pill .sl-zfs-lbl{display:inline;letter-spacing:.02em;white-space:nowrap}
2648
2695
 
2649
2696
  /* toast + boot states (toast flows in the bottom-center region) */
2650
2697
  .sl-toast{transform:translateY(6px) scale(.98);max-width:100%;
@@ -2655,9 +2702,9 @@ var CSS = (
2655
2702
  .sl-toast.has-action{pointer-events:auto;display:flex;align-items:center;gap:12px;padding-right:8px}
2656
2703
  .sl-toast-action{min-height:30px;padding:5px 10px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
2657
2704
  font:inherit;font-weight:800}
2658
- .sl-toast[data-tone="error"]{border-color:#ef4444}
2705
+ .sl-toast[data-tone="error"]{border-color:var(--sl-danger)}
2659
2706
  .sl-toast[data-tone="warning"]{border-color:var(--sl-accent)}
2660
- .sl-toast[data-tone="success"]{border-color:#22c55e}
2707
+ .sl-toast[data-tone="success"]{border-color:var(--sl-success)}
2661
2708
  .sl-toast.on[data-tone="error"]{animation:slToastNudge .32s ease-out}
2662
2709
  .sl-boot{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
2663
2710
  gap:10px;background:var(--sl-bg);font-size:13px;font-weight:600;color:var(--sl-muted)}
@@ -2699,10 +2746,9 @@ var CSS = (
2699
2746
  .sl-booked.on .sl-booked-title{animation-delay:.22s}
2700
2747
  .sl-booked.on .sl-booked-sub{animation-delay:.3s}
2701
2748
 
2702
- /* sold-out overlay \u2014 every SEATED category's live availability is 0. Centered
2703
- over the map; a stub (disabled) "Join waitlist" button, exactly like the page.
2704
- Suppressed when GA areas exist (GA capacity isn't seat-counted). Clears live
2705
- the moment WS frees a seat up. */
2749
+ /* sold-out overlay \u2014 every SEATED category's live availability is 0. This is
2750
+ informational only: no waitlist workflow exists. Suppressed when GA areas
2751
+ exist (GA capacity isn't seat-counted). Clears live when WS frees a seat. */
2706
2752
  .sl-soldout{position:absolute;inset:0;z-index:10;display:none;flex-direction:column;align-items:center;
2707
2753
  justify-content:center;text-align:center;gap:8px;padding:24px;
2708
2754
  background:color-mix(in srgb,var(--sl-bg) 82%,transparent);backdrop-filter:blur(4px)}
@@ -2710,9 +2756,6 @@ var CSS = (
2710
2756
  .sl-soldout-eyebrow{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--sl-accent);font-weight:800}
2711
2757
  .sl-soldout-title{font-size:32px;font-weight:800;color:var(--sl-text);line-height:1.05}
2712
2758
  .sl-soldout-copy{max-width:360px;font-size:13px;color:var(--sl-muted);line-height:1.5}
2713
- .sl-picker .sl-soldout-btn{margin-top:10px;min-height:40px;padding:10px 18px;border-radius:var(--sl-r-sm);
2714
- background:var(--sl-surface);color:var(--sl-muted);border:1px solid var(--sl-line);font-weight:800;font-size:13px;
2715
- cursor:not-allowed;opacity:.85}
2716
2759
 
2717
2760
  /* sales-closed pill (header) \u2014 persistent read-only state when the event's sales
2718
2761
  window is closed at load or closes live mid-session. Neutral (not accent) so it
@@ -2757,7 +2800,7 @@ var CSS = (
2757
2800
  .sl-confirm-field{min-width:0;padding:12px 11px 10px;border-right:1px solid var(--sl-line)}
2758
2801
  .sl-confirm-field:last-child{border-right:0;text-align:center}
2759
2802
  .sl-confirm-field:nth-child(2){text-align:center}
2760
- .sl-confirm-key{display:block;font-size:8.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
2803
+ .sl-confirm-key{display:block;font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
2761
2804
  .sl-confirm-value{display:block;margin-top:4px;color:var(--sl-text);font-size:17px;line-height:1.1;font-weight:850;
2762
2805
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2763
2806
  /* Long venue section names must read in full: smaller type + up to two lines
@@ -2822,7 +2865,7 @@ var CSS = (
2822
2865
  .sl-tip-field{min-width:0;padding:6px 9px;border-right:1px solid var(--sl-line)}
2823
2866
  .sl-tip-field:last-child{border-right:0;text-align:center}
2824
2867
  .sl-tip-grid:not(.one) .sl-tip-field:nth-child(2){text-align:center}
2825
- .sl-tip-key{display:block;font-size:7.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
2868
+ .sl-tip-key{display:block;font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
2826
2869
  .sl-tip-val{display:block;margin-top:2px;color:var(--sl-text);font-size:13px;line-height:1.1;font-weight:750;
2827
2870
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2828
2871
  .sl-tip-cat{display:flex;align-items:center;gap:7px;padding:6px 10px;font-size:11px;
@@ -2830,7 +2873,7 @@ var CSS = (
2830
2873
  .sl-tip-dot{width:8px;height:8px;border-radius:50%;flex:none}
2831
2874
  .sl-tip-name{color:var(--sl-muted);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2832
2875
  .sl-tip-amt{margin-left:auto;font-weight:800;color:var(--sl-text);font-variant-numeric:tabular-nums;font-size:12px}
2833
- .sl-tip-status{padding:5px 10px 7px;font-size:8.5px;letter-spacing:.09em;text-transform:uppercase;font-weight:700;color:var(--sl-muted)}
2876
+ .sl-tip-status{padding:5px 10px 7px;font-size:10px;letter-spacing:.09em;text-transform:uppercase;font-weight:700;color:var(--sl-muted)}
2834
2877
 
2835
2878
  /* Best available is a first-class shortcut, not an anonymous utility row. */
2836
2879
  .sl-ba{position:relative;flex:none;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
@@ -2846,12 +2889,12 @@ var CSS = (
2846
2889
  the confirm popover; deliberately distinct from the accent-toned qty/go. */
2847
2890
  .sl-ba-premium{position:relative;z-index:1;grid-column:1/-1;justify-self:start;display:inline-flex;align-items:center;gap:6px;
2848
2891
  padding:6px 12px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.02em;cursor:pointer;
2849
- color:#c9a24b;background:color-mix(in srgb,#e8c15a 10%,var(--sl-surface));
2850
- border:1px solid color-mix(in srgb,#e8c15a 34%,var(--sl-line));transition:filter .15s,background .15s,color .15s}
2851
- .sl-ba-premium .star{font-size:12px;line-height:1;color:#e8c15a}
2892
+ color:var(--sl-premium-deep);background:color-mix(in srgb,var(--sl-premium) 10%,var(--sl-surface));
2893
+ border:1px solid color-mix(in srgb,var(--sl-premium) 34%,var(--sl-line));transition:filter .15s,background .15s,color .15s}
2894
+ .sl-ba-premium .star{font-size:12px;line-height:1;color:var(--sl-premium)}
2852
2895
  .sl-ba-premium:hover{filter:brightness(1.05)}
2853
- .sl-ba-premium.on{color:#1c1608;background:linear-gradient(135deg,#f0cf6b,#e0b23f);border-color:transparent;
2854
- box-shadow:0 6px 16px color-mix(in srgb,#e8c15a 26%,transparent)}
2896
+ .sl-ba-premium.on{color:var(--sl-premium-ink);background:linear-gradient(135deg,#f0cf6b,#e0b23f);border-color:transparent;
2897
+ box-shadow:0 6px 16px color-mix(in srgb,var(--sl-premium) 26%,transparent)}
2855
2898
  .sl-ba-premium.on .star{color:#5a4410}
2856
2899
  .sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:8px;
2857
2900
  font:inherit;font-size:11px;padding:7px 8px;min-width:0;width:100%;max-width:none}
@@ -2863,7 +2906,15 @@ var CSS = (
2863
2906
  color:var(--sl-accent-ink);font-weight:800;font-size:12px;transition:filter .15s,opacity .15s;display:flex;align-items:center;justify-content:center;gap:6px;
2864
2907
  box-shadow:0 8px 18px color-mix(in srgb,var(--sl-accent) 18%,transparent)}
2865
2908
  .sl-picker .sl-ba-go:hover{filter:brightness(1.06)}
2866
- .sl-picker .sl-ba-go:disabled{opacity:.62;cursor:wait}
2909
+ /* ONE disabled language, shared with the checkout CTA: surface + muted ink +
2910
+ not-allowed. A gold button at 62% still reads as gold, and cursor:wait means
2911
+ "working" \u2014 that pairing made a dead control look live. The wait cursor is
2912
+ reserved for the genuinely in-flight search, which keeps its accent fill
2913
+ via .sl-busy. */
2914
+ .sl-picker .sl-ba-go:disabled{background:var(--sl-surface);color:var(--sl-muted);cursor:not-allowed;
2915
+ filter:none;box-shadow:none}
2916
+ .sl-picker .sl-ba-go.sl-busy:disabled{background:var(--sl-accent);color:var(--sl-accent-ink);opacity:.85;cursor:wait;
2917
+ box-shadow:0 8px 18px color-mix(in srgb,var(--sl-accent) 18%,transparent)}
2867
2918
  .sl-ba-replace{grid-column:1/-1;padding:3px 0 1px}
2868
2919
  .sl-ba-replace b{display:block;font-size:12.5px}
2869
2920
  .sl-ba-replace span{display:block;margin-top:3px;color:var(--sl-muted);font-size:10.5px;line-height:1.35}
@@ -2880,6 +2931,35 @@ var CSS = (
2880
2931
  .sl-picker[data-layout="narrow"] .sl-ba-qty button{width:30px;height:30px}
2881
2932
  .sl-picker[data-layout="narrow"] .sl-ba-go{min-height:40px}
2882
2933
 
2934
+ /* Sales-closed is a designed state, not a set of disables: one statement in
2935
+ the tray where the buying flow was, carrying the event's own date so the
2936
+ buyer knows which night the verdict belongs to. Neutral like the header
2937
+ pill \u2014 "unavailable" never wears the accent. */
2938
+ .sl-closed-note{flex:none;display:flex;flex-direction:column;gap:4px;padding:13px;border-radius:13px;
2939
+ border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-text) 5%,var(--sl-surface))}
2940
+ .sl-closed-note b{font-size:13px;font-weight:850}
2941
+ .sl-closed-note span{font-size:11.5px;line-height:1.45;color:var(--sl-muted)}
2942
+ .sl-closed-note .when{display:block;margin-top:2px;font-size:11.5px;font-weight:700;color:var(--sl-text)}
2943
+
2944
+ /* 44px HIT FLOOR ON TOUCH. Visual sizes stay exactly as designed \u2014 the target
2945
+ grows through an invisible centred pseudo-element, the same grow-the-target-
2946
+ not-the-ink pattern the hosted event page documents for its footer links.
2947
+ Applied only to controls with clearance around them; tightly stacked pairs
2948
+ (the cart chip's remove/view rail) instead get real height on narrow. */
2949
+ @media (pointer:coarse){
2950
+ .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}
2952
+ .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{
2954
+ content:'';position:absolute;top:50%;left:50%;width:max(100%,44px);height:max(100%,44px);
2955
+ transform:translate(-50%,-50%)}
2956
+ /* The chip rail stacks remove over view in ~53px \u2014 pseudo-targets would
2957
+ overlap and make a destructive tap ambiguous. Give the pair real height. */
2958
+ .sl-picker[data-layout="narrow"] .sl-chip{min-height:64px}
2959
+ .sl-picker[data-layout="narrow"] .sl-chip .rm,
2960
+ .sl-picker[data-layout="narrow"] .sl-chip .view{min-height:32px}
2961
+ }
2962
+
2883
2963
  /* screen-reader live region */
2884
2964
  .sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
2885
2965
 
@@ -2914,6 +2994,12 @@ var CSS = (
2914
2994
  Map|3D toggle and the seat confirm both stay usable over it. */
2915
2995
  .sl-view3d{position:absolute;inset:0;z-index:4;opacity:0;touch-action:none;
2916
2996
  transition:opacity .3s ease;background:radial-gradient(120% 120% at 50% 0%,#191f28 0%,#0d1014 70%)}
2997
+ .sl-view3d.has-comparison,.sl-view3d.has-passport{z-index:20}
2998
+ /* Confirm mode normally disables the entire GL sibling. Modal surfaces live
2999
+ inside that sibling, so explicitly restore pointer input only while one is
3000
+ open; their own inert contract keeps the venue underneath unavailable. */
3001
+ .sl-picker[data-confirming="true"] .sl-view3d.has-comparison,
3002
+ .sl-picker[data-confirming="true"] .sl-view3d.has-passport{pointer-events:auto}
2917
3003
  .sl-view3d canvas{display:block;width:100%;height:100%}
2918
3004
  .sl-view3d canvas:focus-visible{outline:2px solid var(--sl-accent);outline-offset:-3px}
2919
3005
  .sl-view3d-loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:10px;
@@ -2922,10 +3008,13 @@ var CSS = (
2922
3008
  border:2px solid rgba(217,226,242,.28);border-top-color:#d9e2f2;animation:slSpin .8s linear infinite}
2923
3009
  [data-view3d=on] .sl-chips,[data-view3d=on] .sl-rungs{display:none}
2924
3010
  .sl-view3d-back{position:absolute;top:12px;left:12px;z-index:2;display:inline-flex;align-items:center;gap:6px;
2925
- padding:7px 13px 7px 9px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.03em;
3011
+ min-height:44px;padding:7px 13px 7px 9px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.03em;
2926
3012
  color:#e6edf3;background:rgba(10,14,20,.62);border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
2927
3013
  .sl-view3d-back:hover,.sl-view3d-back:focus-visible{background:rgba(16,22,30,.82);border-color:rgba(255,255,255,.4)}
2928
3014
  .sl-view3d-back svg{width:15px;height:15px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
3015
+ /* Map|3D already exits an overview to the 2D map. Reserve Back for the one
3016
+ state where it adds meaning: returning from an exact seat to the venue. */
3017
+ .sl-view3d:not(.is-seat-focused) .sl-view3d-back{display:none}
2929
3018
  .sl-view3d-fs{position:absolute;top:12px;right:12px;z-index:2;min-width:44px;min-height:44px;padding:8px 12px;
2930
3019
  border-radius:999px;font-size:16px;color:#e6edf3;background:rgba(10,14,20,.62);
2931
3020
  border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
@@ -2959,6 +3048,7 @@ var CSS = (
2959
3048
  .sl-view3d-nav button:hover,.sl-view3d-nav button:focus-visible{color:#eef1f8;border-color:rgba(190,205,240,.6)}
2960
3049
  .sl-view3d-nav button[aria-pressed="true"]{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
2961
3050
  .sl-view3d-nav button:disabled{opacity:.45;cursor:not-allowed}
3051
+ .sl-view3d-nav-toggle{display:none!important}
2962
3052
  .sl-view3d-nav select{min-height:38px;max-width:100%;padding:7px 34px 7px 12px;
2963
3053
  border-radius:999px;font:700 11.5px/1 inherit;color:#eef1f8;background:rgba(12,18,32,.86);
2964
3054
  border:1px solid rgba(150,165,205,.45);backdrop-filter:blur(6px);cursor:pointer}
@@ -2972,7 +3062,22 @@ var CSS = (
2972
3062
  /* On phones the library owns the bottom edge for seat/panorama/overview actions.
2973
3063
  Keep venue navigation in a separate top rail so those control families never
2974
3064
  stack over one another. */
2975
- .sl-picker[data-layout="narrow"] .sl-view3d-nav{top:68px;bottom:auto;max-width:calc(100% - 24px)}
3065
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav{left:120px;top:12px;bottom:auto;max-width:calc(100% - 132px)}
3066
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav-toggle{display:inline-flex!important;align-items:center;pointer-events:auto;min-height:44px}
3067
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav:not(.is-open)>div{display:none!important}
3068
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav.is-open{left:12px;top:68px;max-width:calc(100% - 24px);padding:8px;
3069
+ border:1px solid rgba(150,165,205,.35);border-radius:14px;background:rgba(8,12,22,.9);backdrop-filter:blur(10px)}
3070
+ .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
+ /* Seat-eye is a decision state, not another venue-navigation state. Once the
3075
+ buyer arrives, clear the floor/area/locator rails and the module's duplicate
3076
+ Overview action. The picker-owned Back button becomes the one predictable
3077
+ escape: seat -> venue -> 2D map. */
3078
+ .sl-view3d.is-seat-focused .sl-view3d-nav,
3079
+ .sl-view3d.is-seat-focused .sl-3d-overview-control,
3080
+ .sl-view3d.is-seat-focused .sl-view3d-compare-saved{display:none!important}
2976
3081
  /* While immersed, the 2D-only chrome is meaningless \u2014 hide it, keep Map|3D. */
2977
3082
  .sl-picker[data-view3d="on"] .sl-rungs,
2978
3083
  .sl-picker[data-view3d="on"] .sl-floors,
@@ -2983,6 +3088,130 @@ var CSS = (
2983
3088
  .sl-picker[data-view3d="on"] .sl-confirm{left:50%!important;top:auto!important;bottom:16px;
2984
3089
  transform:translateX(-50%);width:min(342px,calc(100% - 24px))}
2985
3090
  .sl-picker[data-view3d="on"] .sl-confirm[data-placement]{transform:translateX(-50%)}
3091
+ .sl-confirm-inspect-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:9px}
3092
+ .sl-confirm-inspect-row .sl-confirm-3d{margin-top:0;min-height:44px}
3093
+ .sl-confirm-compare{min-height:44px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
3094
+ display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;font-weight:800;
3095
+ color:var(--sl-text);background:transparent}
3096
+ .sl-confirm-compare:hover,.sl-confirm-compare:focus-visible{border-color:var(--sl-accent);
3097
+ background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
3098
+ .sl-confirm-compare:disabled{opacity:.62;cursor:default}
3099
+ .sl-confirm-confidence{width:100%;min-height:44px;margin-top:8px;padding:8px 10px;border-radius:9px;
3100
+ border:1px solid color-mix(in srgb,var(--sl-accent) 35%,var(--sl-line));display:flex;align-items:center;
3101
+ justify-content:space-between;gap:10px;text-align:left;color:var(--sl-text);
3102
+ background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface))}
3103
+ .sl-confirm-confidence>span{min-width:0}
3104
+ .sl-confirm-confidence strong,.sl-confirm-confidence small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3105
+ .sl-confirm-confidence strong{font-size:11px}.sl-confirm-confidence small{margin-top:2px;color:var(--sl-muted);font-size:9.5px}
3106
+ .sl-confirm-confidence em{display:none;font-style:normal}
3107
+ .sl-confirm-confidence>b{flex:none;font-size:11px;color:var(--sl-accent)}
3108
+ .sl-confirm-confidence:hover,.sl-confirm-confidence:focus-visible{border-color:var(--sl-accent)}
3109
+ /* The production 3D decision dock is deliberately denser than the 2D popup:
3110
+ the venue remains the main content and the two inspection actions share one
3111
+ row. Truth-bearing accessibility/restriction copy is never hidden. */
3112
+ .sl-picker[data-view3d="on"][data-layout="narrow"] .sl-confirm{bottom:10px}
3113
+ .sl-picker[data-view3d="on"][data-layout="narrow"] .sl-confirm-field{padding:8px 9px 7px}
3114
+ .sl-picker[data-view3d="on"][data-layout="narrow"] .sl-confirm-value{font-size:14px;margin-top:2px}
3115
+ .sl-picker[data-view3d="on"][data-layout="narrow"] .sl-confirm-field:first-child .sl-confirm-value{font-size:12px}
3116
+ .sl-picker[data-view3d="on"][data-layout="narrow"] .sl-confirm-cat{padding:7px 10px}
3117
+ .sl-picker[data-view3d="on"][data-layout="narrow"] .sl-confirm-price{font-size:15px}
3118
+ .sl-picker[data-view3d="on"][data-layout="narrow"] .sl-confirm-body{padding:8px 10px 9px}
3119
+ .sl-picker[data-view3d="on"][data-layout="narrow"] .sl-confirm-row{margin-top:7px}
3120
+ /* A short embedded/mobile picker cannot afford a full decision sheet over a
3121
+ 285px map. Keep three 44px action rows and move all disclosure into the
3122
+ passport instead of hiding it without a route back. */
3123
+ .sl-picker[data-view3d="on"][data-layout="narrow"][data-density="compact"] .sl-confirm{bottom:6px}
3124
+ .sl-picker[data-view3d="on"][data-layout="narrow"][data-density="compact"] .sl-confirm-grid,
3125
+ .sl-picker[data-view3d="on"][data-layout="narrow"][data-density="compact"] .sl-confirm-cat,
3126
+ .sl-picker[data-view3d="on"][data-layout="narrow"][data-density="compact"] .sl-confirm-body>.sl-cx{display:none}
3127
+ .sl-picker[data-view3d="on"][data-layout="narrow"][data-density="compact"] .sl-confirm-body{padding:6px 8px 7px}
3128
+ .sl-picker[data-view3d="on"][data-layout="narrow"][data-density="compact"] .sl-confirm-confidence{margin-top:0}
3129
+ .sl-picker[data-view3d="on"][data-layout="narrow"][data-density="compact"] .sl-confirm-confidence em{display:block;margin-bottom:2px;
3130
+ color:var(--sl-text);font-size:12px;font-weight:850;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3131
+ .sl-picker[data-view3d="on"][data-layout="narrow"][data-density="compact"] .sl-confirm-confidence strong{font-size:9.5px}
3132
+ .sl-picker[data-view3d="on"][data-layout="narrow"][data-density="compact"] .sl-confirm-confidence small{display:none}
3133
+ .sl-picker[data-view3d="on"][data-layout="narrow"][data-density="compact"] .sl-confirm-inspect-row{margin-top:5px}
3134
+ .sl-picker[data-view3d="on"][data-layout="narrow"][data-density="compact"] .sl-confirm-row{margin-top:5px}
3135
+
3136
+ /* Saved comparison lives in the top journey row, away from checkout, arrival
3137
+ controls and the bottom-right privacy position. */
3138
+ .sl-view3d-compare-saved{position:absolute;top:12px;left:136px;z-index:5;display:flex;align-items:stretch;
3139
+ max-width:180px;min-height:38px;border-radius:999px;overflow:hidden;color:#e6edf3;
3140
+ background:rgba(10,14,20,.72);border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
3141
+ .sl-view3d-compare-saved button{min-width:0;padding:7px 10px;color:inherit;font-size:11px;font-weight:800;white-space:nowrap}
3142
+ .sl-view3d-compare-saved .main{overflow:hidden;text-overflow:ellipsis}
3143
+ .sl-view3d-compare-saved .clear{width:34px;padding:7px;border-left:1px solid rgba(255,255,255,.18)}
3144
+ .sl-view3d-compare-saved button:hover,.sl-view3d-compare-saved button:focus-visible{background:rgba(255,255,255,.1)}
3145
+ .sl-picker[data-layout="narrow"] .sl-view3d-compare-saved{left:126px;max-width:calc(100% - 194px);min-height:44px}
3146
+
3147
+ /* An unavailable seat is still inspectable. This compact, non-modal status
3148
+ card explains the exact chair the buyer touched without selecting it or
3149
+ obscuring the venue with the full purchase confirmation sheet. */
3150
+ .sl-view3d-unavailable{position:absolute;left:50%;bottom:18px;z-index:9;display:grid;
3151
+ grid-template-columns:minmax(0,1fr) auto;gap:4px 14px;width:min(330px,calc(100% - 24px));
3152
+ padding:13px 14px;border:1px solid rgba(255,255,255,.22);border-radius:14px;
3153
+ color:#eef3fb;background:rgba(10,14,22,.94);box-shadow:0 18px 48px rgba(0,0,0,.48);
3154
+ backdrop-filter:blur(10px);transform:translateX(-50%)}
3155
+ .sl-view3d-unavailable[data-state="held"]{border-color:rgba(242,168,56,.7)}
3156
+ .sl-view3d-unavailable[data-state="sold"],.sl-view3d-unavailable[data-state="dimmed"]{border-color:rgba(160,170,188,.48)}
3157
+ .sl-view3d-unavailable-copy{min-width:0}
3158
+ .sl-view3d-unavailable-eyebrow{display:block;font-size:9px;line-height:1.2;letter-spacing:.13em;
3159
+ text-transform:uppercase;color:#aab7cc;font-weight:850}
3160
+ .sl-view3d-unavailable strong{display:block;margin-top:3px;font-size:17px;line-height:1.2}
3161
+ .sl-view3d-unavailable p{grid-column:1/-1;margin:4px 0 0;color:#b9c4d7;font-size:11px;line-height:1.4}
3162
+ .sl-view3d-unavailable button{align-self:start;min-width:44px;min-height:44px;margin:-5px -6px 0 0;
3163
+ border-radius:999px;color:#eef3fb;font-size:18px;border:1px solid rgba(255,255,255,.18)}
3164
+ .sl-view3d-unavailable button:hover,.sl-view3d-unavailable button:focus-visible{background:rgba(255,255,255,.1)}
3165
+ .sl-picker[data-layout="narrow"] .sl-view3d-unavailable{bottom:10px;padding-bottom:max(13px,env(safe-area-inset-bottom))}
3166
+
3167
+ .sl-view3d-compare-shell{position:absolute;inset:0;z-index:30;display:grid;place-items:center;padding:16px}
3168
+ .sl-view3d-compare-scrim{position:absolute;inset:0;background:rgba(3,6,12,.74);backdrop-filter:blur(5px)}
3169
+ .sl-view3d-compare{position:relative;width:min(720px,100%);max-height:min(680px,calc(100% - 20px));overflow:auto;
3170
+ border:1px solid rgba(160,177,214,.34);border-radius:18px;background:var(--sl-bg);color:var(--sl-text);
3171
+ box-shadow:0 28px 90px rgba(0,0,0,.55);padding:18px}
3172
+ .sl-view3d-compare>header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
3173
+ .sl-view3d-compare>header span{display:block;font-size:9px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:850}
3174
+ .sl-view3d-compare>header strong{display:block;margin-top:4px;font-size:20px}
3175
+ .sl-view3d-compare>header button{min-width:44px;min-height:44px;border-radius:999px;border:1px solid var(--sl-line);color:var(--sl-text)}
3176
+ .sl-view3d-compare-note{margin:12px 0;color:var(--sl-muted);font-size:12px;line-height:1.45}
3177
+ .sl-view3d-compare-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
3178
+ .sl-view3d-compare article{min-width:0;padding:14px;border:1px solid var(--sl-line);border-radius:13px;background:var(--sl-surface)}
3179
+ .sl-view3d-compare article>span{font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);font-weight:850}
3180
+ .sl-view3d-compare article>strong{display:block;margin-top:3px;font-size:20px}
3181
+ .sl-view3d-compare article>small{display:block;margin-top:3px;color:var(--sl-muted)}
3182
+ .sl-view3d-compare dl{margin:12px 0 0}
3183
+ .sl-view3d-compare dl div{display:grid;grid-template-columns:minmax(90px,.8fr) minmax(0,1.2fr);gap:10px;padding:8px 0;border-top:1px solid var(--sl-line)}
3184
+ .sl-view3d-compare dt{font-size:10.5px;color:var(--sl-muted)}
3185
+ .sl-view3d-compare dd{margin:0;text-align:right;font-size:11px;font-weight:750;overflow-wrap:anywhere}
3186
+ .sl-view3d-compare-actions{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;margin-top:12px}
3187
+ .sl-view3d-compare-actions button{min-height:44px;border-radius:9px;border:1px solid var(--sl-line);font-size:12px;font-weight:800}
3188
+ .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}
3190
+ .sl-view3d-passport-shell{position:absolute;inset:0;z-index:40;display:grid;place-items:center;padding:16px}
3191
+ .sl-view3d-passport-scrim{position:absolute;inset:0;background:rgba(3,6,12,.8);backdrop-filter:blur(6px)}
3192
+ .sl-view3d-passport{position:relative;width:min(540px,100%);max-height:min(700px,calc(100% - 20px));overflow:auto;
3193
+ padding:18px;border:1px solid rgba(160,177,214,.38);border-radius:18px;background:var(--sl-bg);color:var(--sl-text);
3194
+ box-shadow:0 28px 90px rgba(0,0,0,.6)}
3195
+ .sl-view3d-passport>header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
3196
+ .sl-view3d-passport>header span{display:block;font-size:9px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:850}
3197
+ .sl-view3d-passport>header strong{display:block;margin-top:4px;font-size:20px}
3198
+ .sl-view3d-passport>header button{min-width:44px;min-height:44px;border:1px solid var(--sl-line);border-radius:999px;color:var(--sl-text)}
3199
+ .sl-view3d-passport-summary{margin:14px 0;padding:12px;border-radius:12px;background:color-mix(in srgb,var(--sl-accent) 9%,var(--sl-surface));
3200
+ border:1px solid color-mix(in srgb,var(--sl-accent) 30%,var(--sl-line))}
3201
+ .sl-view3d-passport-summary strong{display:block;font-size:15px}.sl-view3d-passport-summary span{display:block;margin-top:4px;font-size:11px;color:var(--sl-muted)}
3202
+ .sl-view3d-passport dl{margin:0}.sl-view3d-passport dl div{display:grid;grid-template-columns:minmax(105px,.75fr) minmax(0,1.25fr);
3203
+ gap:12px;padding:9px 0;border-top:1px solid var(--sl-line)}
3204
+ .sl-view3d-passport dt{font-size:10.5px;color:var(--sl-muted)}.sl-view3d-passport dd{margin:0;text-align:right;font-size:11px;font-weight:750;overflow-wrap:anywhere}
3205
+ .sl-view3d-passport h4{margin:14px 0 6px;font-size:11px}.sl-view3d-passport ul{margin:0;padding-left:18px;color:var(--sl-muted);font-size:10.5px;line-height:1.5}
3206
+ .sl-view3d-passport-note{margin:14px 0 0;color:var(--sl-muted);font-size:10.5px;line-height:1.45}
3207
+ @media(max-width:640px){
3208
+ .sl-view3d-compare-shell{padding:max(10px,env(safe-area-inset-top)) 10px max(10px,env(safe-area-inset-bottom))}
3209
+ .sl-view3d-compare{width:100%;max-height:100%;padding:14px 14px max(86px,calc(14px + env(safe-area-inset-bottom)));border-radius:15px}
3210
+ .sl-view3d-compare-grid{grid-template-columns:1fr}
3211
+ .sl-view3d-compare article{padding:12px}
3212
+ .sl-view3d-passport-shell{padding:max(10px,env(safe-area-inset-top)) 10px max(10px,env(safe-area-inset-bottom))}
3213
+ .sl-view3d-passport{width:100%;max-height:100%;padding:14px 14px max(24px,calc(14px + env(safe-area-inset-bottom)));border-radius:15px}
3214
+ }
2986
3215
 
2987
3216
  /* Plain "View from here" action shown when no real photo exists (the synthetic
2988
3217
  thumb is suppressed at card size \u2014 full-screen is where it earns its keep). */
@@ -3100,8 +3329,12 @@ var CSS = (
3100
3329
  .sl-picker[data-layout="narrow"] .sl-minimap{display:none}
3101
3330
 
3102
3331
  /* F4 legend reflection: rows + counts for out-of-band categories read muted */
3103
- .sl-price-row.sl-dim{opacity:.4}
3104
- .sl-seccard-mix-item.sl-dim{opacity:.4}
3332
+ /* Out-of-band rows are exactly the rows a buyer reads while deciding whether
3333
+ to widen a filter: whole-row opacity .4 took the label to ~2:1. Dim the
3334
+ SWATCH and soften the numerals; the words stay legible muted ink. */
3335
+ .sl-price-row.sl-dim .sl-dot{opacity:.35}
3336
+ .sl-price-row.sl-dim .sl-price-label,.sl-price-row.sl-dim .sl-price-left,.sl-price-row.sl-dim .sl-price-amt{color:var(--sl-muted)}
3337
+ .sl-seccard-mix-item.sl-dim{opacity:.55}
3105
3338
 
3106
3339
  /* Buyer-journey motion: every animation explains a state transition (selected,
3107
3340
  held, checkout handoff, conflict or booked). No decorative infinite motion
@@ -3227,6 +3460,8 @@ var SeatPicker = class _SeatPicker {
3227
3460
  // state
3228
3461
  this.currency = "USD";
3229
3462
  this.eventTimezone = null;
3463
+ /** The event's formatted date-time (event-zone), reused by the closed-state note. */
3464
+ this.eventWhenText = null;
3230
3465
  this.offerAvailability = null;
3231
3466
  this.hold = null;
3232
3467
  /** Latest server expiry for the open hold (moves on extend). */
@@ -3282,6 +3517,16 @@ var SeatPicker = class _SeatPicker {
3282
3517
  this.view3dGen = 0;
3283
3518
  /** Seat whose 2D confirm card launched "See it in 3D"; re-shown on return. */
3284
3519
  this.view3dReturnSeat = null;
3520
+ /** Current premium 3D journey depth. `null` is the venue; a seat id is the
3521
+ * fixed seat-eye state. It lets Back unwind one step before leaving 3D. */
3522
+ this.view3dTargetSeatId = null;
3523
+ /** Inspection-only comparison. These ids never represent cart selection. */
3524
+ this.view3dCompareSeatIds = [];
3525
+ this.view3dCompareChip = null;
3526
+ this.view3dCompareEl = null;
3527
+ this.view3dCompareCleanup = null;
3528
+ this.view3dPassportEl = null;
3529
+ this.view3dPassportCleanup = null;
3285
3530
  this.floorsEl = null;
3286
3531
  this.secCardEl = null;
3287
3532
  this.viewEl = null;
@@ -3323,6 +3568,9 @@ var SeatPicker = class _SeatPicker {
3323
3568
  this.fsEscHandler = null;
3324
3569
  /** Host-level event chrome owns the duplicate identity outside full screen. */
3325
3570
  this.eventDetailsHidden = false;
3571
+ /** Wide-layout ticket panel collapsed (map owns the full width). */
3572
+ this.sideCollapsed = false;
3573
+ this.sideToggleEl = null;
3326
3574
  /** True once we've asked the host page to pin us fullscreen (framed, no native). */
3327
3575
  this.framedFs = false;
3328
3576
  /** Last height (px) posted to a host frame; dedupes redundant reports. */
@@ -3632,9 +3880,10 @@ var SeatPicker = class _SeatPicker {
3632
3880
  const active = !!document.fullscreenElement || this.fsFallback || this.framedFs;
3633
3881
  const hideEventDetails = this.eventDetailsHidden && !active;
3634
3882
  this.root?.setAttribute("data-event-details-hidden", String(hideEventDetails));
3635
- this.els.logo?.toggleAttribute("hidden", hideEventDetails);
3636
- this.els.headInfo?.toggleAttribute("hidden", hideEventDetails);
3637
3883
  this.els.zfs?.setAttribute("aria-pressed", String(active));
3884
+ this.els.zfs?.setAttribute("title", active ? "Exit full screen" : "Full screen");
3885
+ const zfsLabel = this.els.zfs?.querySelector(".sl-zfs-lbl");
3886
+ if (zfsLabel) zfsLabel.textContent = active ? "Exit full screen" : "Full screen";
3638
3887
  this.view3dEl?.querySelector(".sl-view3d-fs")?.setAttribute("aria-pressed", String(active));
3639
3888
  }
3640
3889
  /**
@@ -3692,24 +3941,73 @@ var SeatPicker = class _SeatPicker {
3692
3941
  /** Mount the full picker as a document-level modal. Resolves after render. */
3693
3942
  static async open(options) {
3694
3943
  ensureStyle();
3944
+ const priorFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
3695
3945
  const scrim = document.createElement("div");
3696
3946
  scrim.className = "sl-modal-scrim";
3697
3947
  const frame = document.createElement("div");
3698
3948
  frame.className = "sl-modal-frame";
3949
+ frame.setAttribute("role", "dialog");
3950
+ frame.setAttribute("aria-modal", "true");
3951
+ frame.setAttribute("aria-label", "Seat selection");
3952
+ frame.tabIndex = -1;
3699
3953
  scrim.appendChild(frame);
3700
3954
  document.body.appendChild(scrim);
3701
3955
  const prevOverflow = document.body.style.overflow;
3702
3956
  document.body.style.overflow = "hidden";
3703
3957
  const picker = new _SeatPicker({ ...options, container: frame });
3704
3958
  picker.modalScrim = scrim;
3705
- picker.prevFocus = document.activeElement;
3959
+ picker.prevFocus = priorFocus;
3960
+ const focusableSelector = [
3961
+ "a[href]",
3962
+ "area[href]",
3963
+ "button",
3964
+ "input",
3965
+ "select",
3966
+ "textarea",
3967
+ "iframe",
3968
+ "object",
3969
+ "embed",
3970
+ "summary",
3971
+ "audio[controls]",
3972
+ "video[controls]",
3973
+ '[contenteditable]:not([contenteditable="false"])',
3974
+ "[tabindex]"
3975
+ ].join(",");
3976
+ const activeDialog = () => {
3977
+ const nested = [...frame.querySelectorAll('[role="dialog"][aria-modal="true"]')].filter((dialog) => dialog.isConnected && !dialog.closest('[hidden], [aria-hidden="true"], [inert]'));
3978
+ return nested[nested.length - 1] ?? frame;
3979
+ };
3980
+ const hiddenWithin = (element, scope) => {
3981
+ let current = element;
3982
+ while (current) {
3983
+ const style = window.getComputedStyle(current);
3984
+ if (current.hidden || current.getAttribute("aria-hidden") === "true" || current.hasAttribute("inert") || style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") return true;
3985
+ if (current === scope) return false;
3986
+ current = current.parentElement;
3987
+ }
3988
+ return true;
3989
+ };
3990
+ const tabbableWithin = (scope) => [...scope.querySelectorAll(focusableSelector)].filter((element) => element.tabIndex >= 0 && !element.matches(":disabled") && !hiddenWithin(element, scope));
3991
+ const focusEdge = (scope, backwards) => {
3992
+ const tabbable = tabbableWithin(scope);
3993
+ const target = backwards ? tabbable[tabbable.length - 1] : tabbable[0];
3994
+ if (target) target.focus({ preventScroll: true });
3995
+ else {
3996
+ if (!scope.hasAttribute("tabindex")) scope.tabIndex = -1;
3997
+ scope.focus({ preventScroll: true });
3998
+ }
3999
+ };
3706
4000
  let closing = false;
3707
4001
  const close = () => {
3708
4002
  if (closing) return;
3709
4003
  closing = true;
3710
4004
  document.body.style.overflow = prevOverflow;
3711
- scrim.style.opacity = "0";
3712
- scrim.style.pointerEvents = "none";
4005
+ if (picker.escHandler) document.removeEventListener("keydown", picker.escHandler);
4006
+ scrim.remove();
4007
+ picker.modalScrim = null;
4008
+ const restoreTarget = picker.prevFocus;
4009
+ picker.prevFocus = null;
4010
+ if (restoreTarget?.isConnected) restoreTarget.focus({ preventScroll: true });
3713
4011
  const finish = () => {
3714
4012
  picker.destroy();
3715
4013
  options.onClose?.();
@@ -3722,6 +4020,28 @@ var SeatPicker = class _SeatPicker {
3722
4020
  if (e.target === scrim) close();
3723
4021
  });
3724
4022
  picker.escHandler = (e) => {
4023
+ if (e.key === "Tab") {
4024
+ if (e.defaultPrevented) return;
4025
+ const scope = activeDialog();
4026
+ const tabbable = tabbableWithin(scope);
4027
+ const first = tabbable[0];
4028
+ const last = tabbable[tabbable.length - 1];
4029
+ const active = document.activeElement;
4030
+ if (!first || !last) {
4031
+ e.preventDefault();
4032
+ focusEdge(scope, e.shiftKey);
4033
+ } else if (active === scope || !active || !scope.contains(active)) {
4034
+ e.preventDefault();
4035
+ (e.shiftKey ? last : first).focus({ preventScroll: true });
4036
+ } else if (e.shiftKey && active === first) {
4037
+ e.preventDefault();
4038
+ last.focus({ preventScroll: true });
4039
+ } else if (!e.shiftKey && active === last) {
4040
+ e.preventDefault();
4041
+ first.focus({ preventScroll: true });
4042
+ }
4043
+ return;
4044
+ }
3725
4045
  if (e.key !== "Escape") return;
3726
4046
  if (picker.tableDialog) {
3727
4047
  e.preventDefault();
@@ -3734,6 +4054,7 @@ var SeatPicker = class _SeatPicker {
3734
4054
  picker.bestAvailableConfirm = false;
3735
4055
  picker.syncTray();
3736
4056
  } else {
4057
+ e.preventDefault();
3737
4058
  close();
3738
4059
  }
3739
4060
  };
@@ -3741,6 +4062,7 @@ var SeatPicker = class _SeatPicker {
3741
4062
  await picker.render();
3742
4063
  picker.els.close?.classList.add("on");
3743
4064
  picker.els.close?.addEventListener("click", close);
4065
+ focusEdge(activeDialog(), false);
3744
4066
  return picker;
3745
4067
  }
3746
4068
  async render() {
@@ -3794,13 +4116,14 @@ var SeatPicker = class _SeatPicker {
3794
4116
  <div class="sl-map">
3795
4117
  <div class="sl-map-host" data-ref="map"></div>
3796
4118
  <div class="sl-zoom" data-ref="zoom">
3797
- <button type="button" aria-label="Zoom in" data-ref="zin">+</button>
3798
- <button type="button" aria-label="Zoom out" data-ref="zout">\u2212</button>
3799
- <button type="button" aria-label="Fit to screen" data-ref="zfit">
4119
+ <button type="button" aria-label="Zoom in" title="Zoom in" data-ref="zin">+</button>
4120
+ <button type="button" aria-label="Zoom out" title="Zoom out" data-ref="zout">\u2212</button>
4121
+ <button type="button" aria-label="Fit to screen" title="Fit to screen" data-ref="zfit">
3800
4122
  <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>
3801
4123
  </button>
3802
- <button type="button" aria-label="Full screen" aria-pressed="false" data-ref="zfs">
4124
+ <button type="button" aria-label="Full screen" title="Full screen" aria-pressed="false" data-ref="zfs">
3803
4125
  <svg viewBox="0 0 24 24"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
4126
+ <span class="sl-zfs-lbl" aria-hidden="true">Full screen</span>
3804
4127
  </button>
3805
4128
  </div>
3806
4129
  <div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
@@ -3852,8 +4175,10 @@ var SeatPicker = class _SeatPicker {
3852
4175
  if (w <= 0) return;
3853
4176
  this.reportFramedHeight();
3854
4177
  const next = w < 640 ? "narrow" : "wide";
3855
- if (root.dataset.layout === next) return;
4178
+ const density = root.clientHeight < 560 ? "compact" : "comfortable";
4179
+ if (root.dataset.layout === next && root.dataset.density === density) return;
3856
4180
  root.dataset.layout = next;
4181
+ root.dataset.density = density;
3857
4182
  if (next === "narrow" && !root.dataset.sheet) root.dataset.sheet = "peek";
3858
4183
  this.dockLayoutChrome();
3859
4184
  };
@@ -3962,17 +4287,18 @@ var SeatPicker = class _SeatPicker {
3962
4287
  badge.className = "sl-testbadge";
3963
4288
  badge.textContent = t2("picker.testMode");
3964
4289
  badge.setAttribute("aria-label", t2("picker.testMode"));
3965
- this.els.map.appendChild(badge);
4290
+ this.regions["top-left"].appendChild(badge);
3966
4291
  }
3967
4292
  const chartTheme = this.controller.doc?.theme;
3968
4293
  Object.entries(resolveTokens(chartTheme, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
3969
4294
  this.currency = info.currency ?? this.opts.currency ?? "USD";
3970
4295
  this.eventTimezone = info.timezone ?? null;
3971
- const logoUrl = this.opts.theme?.logoUrl ?? chartTheme?.logoUrl;
4296
+ const logoUrl = this.opts.theme?.logoUrl ?? chartTheme?.logoUrl ?? info.posterUrl ?? null;
3972
4297
  if (logoUrl) this.els.logo.innerHTML = `<img src="${logoUrl}" alt="">`;
3973
4298
  else this.els.logo.textContent = (this.opts.theme?.brandName ?? chartTheme?.brandName ?? info.eventName ?? "?").slice(0, 1).toUpperCase();
3974
4299
  this.els.name.textContent = info.eventName ?? "";
3975
4300
  const when = info.startsAt ? formatWhen(info.startsAt, info.timezone ?? null, this.opts.locale) : "";
4301
+ this.eventWhenText = when || null;
3976
4302
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
3977
4303
  this.buildBadge(chartTheme);
3978
4304
  const present = /* @__PURE__ */ new Set();
@@ -4063,6 +4389,8 @@ var SeatPicker = class _SeatPicker {
4063
4389
  this.buildExtendPrompt();
4064
4390
  this.buildBookedOverlay();
4065
4391
  this.buildSoldoutOverlay();
4392
+ this.buildPanelToggle();
4393
+ if (this.opts.panelCollapsed) this.setPanelCollapsed(true);
4066
4394
  this.dockLayoutChrome();
4067
4395
  await this.restoreRememberedHold();
4068
4396
  if (this.destroyed) return this;
@@ -4109,8 +4437,63 @@ var SeatPicker = class _SeatPicker {
4109
4437
  * re-render the section card in the form the layout wants (docked card/pill
4110
4438
  * on wide, sheet strip on narrow). Runs on every layout flip + once post-render.
4111
4439
  */
4440
+ /**
4441
+ * The wide-layout panel toggle: a labelled pill in the top-right region
4442
+ * beside Map|3D. Collapsing gives the map the panel's 300px; the pill's own
4443
+ * label is the way back ("Tickets"), and syncTray reopens the panel when the
4444
+ * first seat lands so the checkout can never be hidden behind a collapse.
4445
+ */
4446
+ buildPanelToggle() {
4447
+ if (!this.regions["top-right"]) return;
4448
+ const btn = document.createElement("button");
4449
+ btn.type = "button";
4450
+ btn.className = "sl-side-toggle";
4451
+ btn.addEventListener("click", () => this.setPanelCollapsed(!this.sideCollapsed));
4452
+ this.sideToggleEl = btn;
4453
+ this.regions["top-right"].appendChild(btn);
4454
+ this.syncPanelToggle();
4455
+ }
4456
+ syncPanelToggle() {
4457
+ const btn = this.sideToggleEl;
4458
+ if (!btn) return;
4459
+ const collapsed = this.sideCollapsed;
4460
+ btn.setAttribute("aria-expanded", String(!collapsed));
4461
+ btn.setAttribute("aria-label", collapsed ? "Show the ticket panel" : "Hide the ticket panel");
4462
+ 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>';
4463
+ }
4464
+ /**
4465
+ * Collapse or reopen the wide-layout ticket panel in place (host-drivable —
4466
+ * a map-first embed can mount collapsed via `panelCollapsed` and reopen on
4467
+ * its own cue). Inert while collapsed so a keyboard buyer cannot tab into a
4468
+ * zero-width panel; narrow layouts ignore the state entirely (the sheet is
4469
+ * already the collapse) but keep it for the next flip back to wide.
4470
+ */
4471
+ setPanelCollapsed(collapsed) {
4472
+ if (this.destroyed) return;
4473
+ this.sideCollapsed = collapsed;
4474
+ this.applyPanelCollapsed();
4475
+ this.scheduleMotion(() => this.controller.zoomToFit(), 340);
4476
+ }
4477
+ /** Idempotent DOM apply, also run on every layout flip (inert must lift on narrow). */
4478
+ applyPanelCollapsed() {
4479
+ const wide = this.root?.dataset.layout !== "narrow";
4480
+ this.root?.setAttribute("data-side-collapsed", String(this.sideCollapsed));
4481
+ this.els.side?.toggleAttribute("inert", this.sideCollapsed && wide);
4482
+ this.syncPanelToggle();
4483
+ }
4112
4484
  dockLayoutChrome() {
4113
4485
  const narrow = this.root?.dataset.layout === "narrow";
4486
+ this.applyPanelCollapsed();
4487
+ const zfs = this.els.zfs;
4488
+ if (zfs) {
4489
+ if (narrow && this.regions["top-right"]) {
4490
+ zfs.classList.add("sl-fs-pill");
4491
+ this.regions["top-right"].appendChild(zfs);
4492
+ } else if (!narrow && this.els.zoom && zfs.parentElement !== this.els.zoom) {
4493
+ zfs.classList.remove("sl-fs-pill");
4494
+ this.els.zoom.appendChild(zfs);
4495
+ }
4496
+ }
4114
4497
  const filters = this.els.filters;
4115
4498
  if (filters) {
4116
4499
  if (this.cbEl) this.els.zoom?.appendChild(this.cbEl);
@@ -4158,14 +4541,14 @@ var SeatPicker = class _SeatPicker {
4158
4541
  const v = t2(key);
4159
4542
  return v === key ? fallback : v;
4160
4543
  }
4161
- /** Sold-out overlay — centered over the map, disabled waitlist stub (Gap 2). */
4544
+ /** Sold-out overlay — an informational state with no unavailable action. */
4162
4545
  buildSoldoutOverlay() {
4163
4546
  if (!this.els.map) return;
4164
4547
  const el = document.createElement("div");
4165
4548
  el.className = "sl-soldout";
4166
4549
  el.setAttribute("role", "status");
4167
4550
  const name = (this.controller.doc?.theme?.brandName ?? this.opts.theme?.brandName ?? this.els.name?.textContent ?? this.tf("picker.soldOutEyebrow", "This event")).toUpperCase();
4168
- el.innerHTML = `<div class="sl-soldout-eyebrow">${name}</div><div class="sl-soldout-title">${this.tf("picker.soldOutTitle", "Sold out")}</div><p class="sl-soldout-copy">${this.tf("picker.soldOutCopy", "Every seat is gone. Join the waitlist and we\u2019ll email you if seats are released.")}</p><button type="button" class="sl-soldout-btn" disabled>${this.tf("picker.waitlist", "Join waitlist")}</button>`;
4551
+ el.innerHTML = `<div class="sl-soldout-eyebrow">${name}</div><div class="sl-soldout-title">${this.tf("picker.soldOutTitle", "Sold out")}</div><p class="sl-soldout-copy">${this.tf("picker.soldOutCopy", "No reserved seats are currently available for this event.")}</p>`;
4169
4552
  this.els.map.appendChild(el);
4170
4553
  this.soldoutEl = el;
4171
4554
  }
@@ -4213,6 +4596,9 @@ var SeatPicker = class _SeatPicker {
4213
4596
  text.textContent = this.tf("picker.salesClosedPill", "Sales are closed");
4214
4597
  }
4215
4598
  this.root?.setAttribute("data-sales-closed", String(this.salesClosed));
4599
+ if (this.salesClosed && this.srEl) {
4600
+ this.srEl.textContent = this.tf("picker.salesClosedToast", "Sales are closed for this event.");
4601
+ }
4216
4602
  this.syncCta();
4217
4603
  this.syncTray();
4218
4604
  }
@@ -5058,6 +5444,8 @@ var SeatPicker = class _SeatPicker {
5058
5444
  this.confirmEl = null;
5059
5445
  this.confirmSeat = seat;
5060
5446
  this.root?.setAttribute("data-confirming", "true");
5447
+ this.els.side?.toggleAttribute("inert", true);
5448
+ Object.values(this.regions).forEach((region) => region.toggleAttribute("inert", true));
5061
5449
  this.controller.setSelectionFocus(seat.id);
5062
5450
  if (previousId && previousId !== seat.id) this.controller.deselect([previousId]);
5063
5451
  if (this.tipEl) this.tipEl.style.display = "none";
@@ -5079,10 +5467,9 @@ var SeatPicker = class _SeatPicker {
5079
5467
  const el = document.createElement("div");
5080
5468
  el.className = "sl-confirm";
5081
5469
  el.setAttribute("role", "dialog");
5082
- el.setAttribute("aria-modal", "true");
5083
5470
  el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
5084
5471
  el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
5085
- 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.confirmThumbHtml(seat) : "") + 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>`;
5472
+ 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>`;
5086
5473
  this.els.map.appendChild(el);
5087
5474
  this.confirmEl = el;
5088
5475
  const thumb = el.querySelector(".sl-confirm-thumb");
@@ -5094,6 +5481,10 @@ var SeatPicker = class _SeatPicker {
5094
5481
  }
5095
5482
  this.reanchorConfirm();
5096
5483
  el.querySelector(".sl-confirm-view")?.addEventListener("click", () => void this.openSeatView(seat));
5484
+ el.querySelector(".sl-confirm-confidence")?.addEventListener("click", (event) => {
5485
+ this.openSeatConfidencePassport(seat, event.currentTarget instanceof HTMLElement ? event.currentTarget : null);
5486
+ });
5487
+ el.querySelector(".sl-confirm-compare")?.addEventListener("click", () => this.saveView3dComparisonSeat(seat));
5097
5488
  el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
5098
5489
  if (this.buyerView === "venue3d") {
5099
5490
  this.dismissConfirm();
@@ -5130,6 +5521,8 @@ var SeatPicker = class _SeatPicker {
5130
5521
  this.confirmEl = null;
5131
5522
  this.confirmSeat = null;
5132
5523
  this.root?.removeAttribute("data-confirming");
5524
+ Object.values(this.regions).forEach((region) => region.toggleAttribute("inert", false));
5525
+ this.applyPanelCollapsed();
5133
5526
  this.controller.setSelectionFocus(null);
5134
5527
  }
5135
5528
  commitConfirm() {
@@ -5137,12 +5530,14 @@ var SeatPicker = class _SeatPicker {
5137
5530
  this.dismissConfirm();
5138
5531
  this.collapseSectionCard();
5139
5532
  this.syncTray();
5533
+ this.syncSelectionTo3d();
5140
5534
  }
5141
5535
  cancelConfirm() {
5142
5536
  const seat = this.confirmSeat;
5143
5537
  if (!seat) return;
5144
5538
  this.controller.deselect([seat.id]);
5145
5539
  if (this.confirmSeat) this.dismissConfirm();
5540
+ this.syncSelectionTo3d();
5146
5541
  this.root?.focus({ preventScroll: true });
5147
5542
  }
5148
5543
  closeConfirm() {
@@ -5566,19 +5961,24 @@ var SeatPicker = class _SeatPicker {
5566
5961
  const heldItems = this.hold?.items ?? [];
5567
5962
  const parts = [];
5568
5963
  const nextTrayKeys = /* @__PURE__ */ new Set();
5569
- if (!seats.length && !heldItems.length && !gaAreas.length) {
5964
+ if (this.salesClosed && !seats.length && !heldItems.length) {
5965
+ const closedWhen = this.eventWhenText ? `<span class="when">${String(this.eventWhenText).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c])}</span>` : "";
5966
+ parts.push(
5967
+ `<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>`
5968
+ );
5969
+ } else if (!seats.length && !heldItems.length && !gaAreas.length) {
5570
5970
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map, or let us pick the best available for you.</div>`);
5571
5971
  } else if (!seats.length && !heldItems.length) {
5572
5972
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
5573
5973
  }
5574
5974
  const noPicks = !seats.length && !heldItems.length && !this.pendingGACount();
5575
- if (!this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
5975
+ if (!this.salesClosed && !this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
5576
5976
  const cats = this.controller.doc?.categories ?? [];
5577
5977
  const zones = this.controller.getBestAvailableZones();
5578
5978
  if (this.baZone && !zones.some((zone) => zone.id === this.baZone)) this.baZone = "";
5579
5979
  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
5580
5980
  // seats (same present-only philosophy as the a11y filter chips).
5581
- (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 ? " 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>`);
5981
+ (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>`);
5582
5982
  }
5583
5983
  const idGrid = (seatId, label, objectType, quantity = 1, objectId, identity) => {
5584
5984
  const esc = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
@@ -5788,11 +6188,16 @@ var SeatPicker = class _SeatPicker {
5788
6188
  }
5789
6189
  if (count !== previousCount) this.animateOnce(this.els.count, "sl-value-pop", 380);
5790
6190
  if (total !== previousTotal) this.animateOnce(this.els.total, "sl-value-pop", 380);
5791
- if (previousCount === 0 && count > 0) this.animateOnce(this.els.cta, "sl-ready", 520);
6191
+ if (previousCount === 0 && count > 0) {
6192
+ this.animateOnce(this.els.cta, "sl-ready", 520);
6193
+ if (this.sideCollapsed && this.root?.dataset.layout !== "narrow") this.setPanelCollapsed(false);
6194
+ }
5792
6195
  if (this.els.peek) {
5793
6196
  if (count) {
5794
6197
  const holding = !!this.hold && !pendingCount;
5795
6198
  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>`;
6199
+ } else if (this.salesClosed) {
6200
+ this.els.peek.innerHTML = `<span>${this.tf("picker.salesClosedPill", "Sales are closed")}</span>`;
5796
6201
  } else {
5797
6202
  const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
5798
6203
  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>`;
@@ -6439,16 +6844,13 @@ var SeatPicker = class _SeatPicker {
6439
6844
  return null;
6440
6845
  }
6441
6846
  }
6442
- const doc = this.controller.doc;
6443
- if (!doc) return null;
6444
- const activeId = this.controller.getActiveFloorId();
6445
- const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
6446
- try {
6447
- const { generateSeatPanorama } = await loadPanorama();
6448
- return { url: generateSeatPanorama(seat, focal, this.allSeats()).url, generated: true };
6449
- } catch {
6450
- return null;
6451
- }
6847
+ return {
6848
+ url: "",
6849
+ generated: true,
6850
+ mediaKind: "model",
6851
+ coverage: "exact-seat",
6852
+ sourceLabel: this.tf("picker.chartDerivedModel", "Chart-derived model")
6853
+ };
6452
6854
  }
6453
6855
  /** Route the module's decoupled analytics into the host callback, tagged buyer. */
6454
6856
  emit3dAnalytics(event, props) {
@@ -6472,8 +6874,16 @@ var SeatPicker = class _SeatPicker {
6472
6874
  if (already) {
6473
6875
  this.controller.deselect([seatId]);
6474
6876
  this.dismissConfirm();
6877
+ this.syncSelectionTo3d();
6475
6878
  return;
6476
6879
  }
6880
+ const visualState = this.seatState3dFor(seat);
6881
+ if (visualState !== "available") {
6882
+ this.showUnavailable3dSeat(seat, visualState);
6883
+ this.syncSelectionTo3d();
6884
+ return;
6885
+ }
6886
+ this.dismissUnavailable3dSeat();
6477
6887
  const added = this.controller.select([seatId]);
6478
6888
  if (!added.length) {
6479
6889
  this.syncSelectionTo3d();
@@ -6489,6 +6899,391 @@ var SeatPicker = class _SeatPicker {
6489
6899
  }
6490
6900
  if (this.opts.confirmSelection !== false) this.showConfirm(seat);
6491
6901
  else this.syncTray();
6902
+ this.syncSelectionTo3d();
6903
+ }
6904
+ /** Explain a visible-but-unselectable 3D seat without entering the booking
6905
+ * flow. Category colour remains visible in the venue; this card names the
6906
+ * availability state explicitly so yellow never has to carry both meanings. */
6907
+ showUnavailable3dSeat(seat, visualState) {
6908
+ const overlay = this.view3dEl;
6909
+ if (!overlay) return;
6910
+ this.dismissUnavailable3dSeat();
6911
+ const status = this.controller.getStatus(seat.id);
6912
+ const details = this.controller.seatDetails(seat.id);
6913
+ const identity = details?.displayLabel ?? seat.displayLabel ?? seat.label;
6914
+ const section = details?.sectionLabel;
6915
+ const row = this.rowShort(details);
6916
+ const location2 = [section, row ? `Row ${row}` : null, identity].filter(Boolean).join(" \xB7 ");
6917
+ const copy = status === "held" ? {
6918
+ title: this.tf("picker.temporarilyHeld", "Temporarily held"),
6919
+ message: this.tf("picker.heldSeatExplanation", "Another buyer is holding this seat. It may become available again.")
6920
+ } : status === "booked" ? {
6921
+ title: this.tf("picker.sold", "Sold"),
6922
+ message: this.tf("picker.soldSeatExplanation", "This seat has already been booked.")
6923
+ } : status === "not_for_sale" ? {
6924
+ title: this.tf("picker.notForSale", "Not for sale"),
6925
+ message: this.tf("picker.notForSaleExplanation", "This seat is not included in the current sale.")
6926
+ } : {
6927
+ title: this.tf("picker.filteredSeat", "Unavailable with current filters"),
6928
+ message: this.tf("picker.filteredSeatExplanation", "Change the active price or view filters to make this seat selectable.")
6929
+ };
6930
+ const card = document.createElement("div");
6931
+ card.className = "sl-view3d-unavailable";
6932
+ card.dataset.state = visualState;
6933
+ card.setAttribute("role", "status");
6934
+ card.setAttribute("aria-live", "polite");
6935
+ const eyebrow = document.createElement("span");
6936
+ eyebrow.className = "sl-view3d-unavailable-eyebrow";
6937
+ eyebrow.textContent = location2 || identity;
6938
+ const title = document.createElement("strong");
6939
+ title.textContent = copy.title;
6940
+ const content = document.createElement("div");
6941
+ content.className = "sl-view3d-unavailable-copy";
6942
+ content.append(eyebrow, title);
6943
+ const close = document.createElement("button");
6944
+ close.type = "button";
6945
+ close.setAttribute("aria-label", this.tf("picker.closeSeatStatus", "Close seat status"));
6946
+ close.textContent = "\xD7";
6947
+ const message = document.createElement("p");
6948
+ message.textContent = copy.message;
6949
+ card.append(content, close, message);
6950
+ close.addEventListener("click", () => card.remove());
6951
+ overlay.appendChild(card);
6952
+ this.announceSeat(seat);
6953
+ }
6954
+ dismissUnavailable3dSeat() {
6955
+ this.view3dEl?.querySelector(".sl-view3d-unavailable")?.remove();
6956
+ }
6957
+ /** Comparison belongs to inspection, never selection. The confirm candidate
6958
+ * remains excluded from checkout until Select; saving it releases that
6959
+ * candidate before any comparison state is created. */
6960
+ view3dCompareConfirmHtml(seat) {
6961
+ if (this.buyerView !== "venue3d") return "";
6962
+ const saved = this.view3dCompareSeatIds;
6963
+ const included = saved.includes(seat.id);
6964
+ const label = included ? saved.length > 1 ? this.tf("picker.openComparison", "Open comparison") : this.tf("picker.savedForComparison", "Saved for comparison") : saved.length ? this.tf("picker.compareWithSaved", "Compare with saved") : this.tf("picker.saveToCompare", "Save to compare");
6965
+ return `<button type="button" class="sl-confirm-compare"${included && saved.length === 1 ? " disabled" : ""}><span aria-hidden="true">\u21C4</span><span>${this.escCx(label)}</span></button>`;
6966
+ }
6967
+ seatConfidenceConfirmHtml(seat, compactSummary) {
6968
+ if (this.buyerView !== "venue3d") return "";
6969
+ const disclosure = seatConfidenceDisclosure(seat.confidenceEvidence);
6970
+ const detail = disclosure.modeledTarget ?? disclosure.reality;
6971
+ return `<button type="button" class="sl-confirm-confidence" aria-label="Open seat confidence passport for ${this.escCx(seat.displayLabel ?? seat.label)}"><span><em>${this.escCx(compactSummary)}</em><strong>${this.escCx(disclosure.headline)}</strong><small>${this.escCx(detail)}</small></span><b>Passport</b></button>`;
6972
+ }
6973
+ saveView3dComparisonSeat(seat) {
6974
+ if (this.buyerView !== "venue3d") return;
6975
+ const previous = this.view3dCompareSeatIds;
6976
+ if (!previous.includes(seat.id)) {
6977
+ this.view3dCompareSeatIds = previous.length === 0 ? [seat.id] : [previous[0], seat.id];
6978
+ }
6979
+ if (this.confirmSeat?.id === seat.id) {
6980
+ this.controller.deselect([seat.id]);
6981
+ this.dismissConfirm();
6982
+ this.syncSelectionTo3d();
6983
+ this.syncTray();
6984
+ }
6985
+ this.syncView3dCompareChip();
6986
+ this.emit3dAnalytics("3d_comparison_saved", {
6987
+ seatId: seat.id,
6988
+ count: this.view3dCompareSeatIds.length
6989
+ });
6990
+ if (this.view3dCompareSeatIds.length > 1) this.openView3dComparison();
6991
+ else this.toast(this.tf("picker.chooseAnotherToCompare", "Seat saved. Choose another seat to compare."), "success");
6992
+ }
6993
+ clearView3dComparison() {
6994
+ this.closeView3dComparison(false);
6995
+ this.view3dCompareSeatIds = [];
6996
+ this.view3dCompareChip?.remove();
6997
+ this.view3dCompareChip = null;
6998
+ this.emit3dAnalytics("3d_comparison_cleared");
6999
+ }
7000
+ syncView3dCompareChip() {
7001
+ const overlay = this.view3dEl;
7002
+ if (!overlay || this.view3dCompareSeatIds.length === 0) {
7003
+ this.view3dCompareChip?.remove();
7004
+ this.view3dCompareChip = null;
7005
+ return;
7006
+ }
7007
+ let chip = this.view3dCompareChip;
7008
+ if (!chip) {
7009
+ chip = document.createElement("div");
7010
+ chip.className = "sl-view3d-compare-saved";
7011
+ chip.setAttribute("role", "group");
7012
+ chip.setAttribute("aria-label", "Saved seat comparison");
7013
+ const main2 = document.createElement("button");
7014
+ main2.type = "button";
7015
+ main2.className = "main";
7016
+ main2.addEventListener("click", () => {
7017
+ if (this.view3dCompareSeatIds.length > 1) this.openView3dComparison();
7018
+ else this.toast(this.tf("picker.chooseAnotherToCompare", "Choose another seat to compare."), "neutral");
7019
+ });
7020
+ const clear = document.createElement("button");
7021
+ clear.type = "button";
7022
+ clear.className = "clear";
7023
+ clear.textContent = "\xD7";
7024
+ clear.setAttribute("aria-label", "Clear saved seat comparison");
7025
+ clear.addEventListener("click", () => this.clearView3dComparison());
7026
+ chip.append(main2, clear);
7027
+ overlay.appendChild(chip);
7028
+ this.view3dCompareChip = chip;
7029
+ }
7030
+ const count = this.view3dCompareSeatIds.length;
7031
+ const main = chip.querySelector(".main");
7032
+ if (main) {
7033
+ main.textContent = count > 1 ? `Compare ${count}` : "1 seat saved";
7034
+ main.setAttribute("aria-label", count > 1 ? `Open comparison of ${count} seats` : "One seat saved; choose another to compare");
7035
+ }
7036
+ }
7037
+ view3dComparisonSnapshot(seatId) {
7038
+ const seat = this.allSeats().find((candidate) => candidate.id === seatId);
7039
+ if (!seat) return null;
7040
+ const details = this.controller.seatDetails(seat.id);
7041
+ const cat = this.controller.doc?.categories.find((candidate) => candidate.key === seat.categoryKey);
7042
+ const chartPrice = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
7043
+ const price = chartPrice != null ? this.paidPrice(seat.categoryKey, details?.tierId ?? cat?.tiers?.[0]?.id ?? null, chartPrice) : void 0;
7044
+ const status = this.controller.getStatus(seat.id);
7045
+ const availability = status === "held" ? this.tf("picker.temporarilyHeld", "Temporarily held") : status === "booked" ? this.tf("picker.sold", "Sold") : status === "not_for_sale" ? this.tf("picker.notForSale", "Not for sale") : this.tf("picker.available", "Available");
7046
+ const viewSource = seat.viewUrl ? seatViewDisclosure({
7047
+ url: seat.viewUrl,
7048
+ ...seat.viewMeta?.coverage ? { coverage: seat.viewMeta.coverage } : {},
7049
+ ...seat.viewMeta?.capturedAt ? { capturedAt: seat.viewMeta.capturedAt } : {},
7050
+ ...seat.viewMeta?.sourceLabel ? { sourceLabel: seat.viewMeta.sourceLabel } : {}
7051
+ }) : this.tf("picker.chartDerivedSeatEye", "Live 3D \xB7 chart-derived seat-eye \xB7 not surveyed");
7052
+ const limited = this.limitedViewLabel(seat.commercial) || this.tf("picker.noAuthoredRestriction", "No organizer-authored restriction");
7053
+ const accessibility = details?.wheelchairSpaceType ? `${this.wheelchairProvisionLabel(details.wheelchairSpaceType)} \xB7 metadata, not access certification` : this.tf("picker.noAccessibilityMetadata", "No accessibility metadata supplied");
7054
+ const confidence = seatConfidenceDisclosure(seat.confidenceEvidence);
7055
+ return {
7056
+ seat,
7057
+ label: details?.displayLabel ?? seat.displayLabel ?? seat.label,
7058
+ section: details?.sectionLabel ?? seat.sectionId ?? "\u2014",
7059
+ row: this.rowShort(details) ?? details?.rowLabel ?? "\u2014",
7060
+ category: details?.categoryLabel ?? cat?.label ?? seat.categoryKey,
7061
+ price: price == null ? this.tf("picker.priceNotSupplied", "Not supplied") : this.money(price),
7062
+ availability,
7063
+ selectable: status == null || status === "free",
7064
+ viewSource,
7065
+ limited,
7066
+ accessibility,
7067
+ confidence
7068
+ };
7069
+ }
7070
+ openSeatConfidencePassport(seat, returnFocus = null) {
7071
+ const overlay = this.view3dEl;
7072
+ if (!overlay) return;
7073
+ this.closeSeatConfidencePassport(false);
7074
+ const disclosure = seatConfidenceDisclosure(seat.confidenceEvidence);
7075
+ const evidence = seat.confidenceEvidence;
7076
+ const details = this.controller.seatDetails(seat.id);
7077
+ const safe = (value) => this.escCx(value);
7078
+ const limitations = disclosure.limitations.length ? `<h4>Known limits</h4><ul>${disclosure.limitations.map((item) => `<li>${safe(item)}</li>`).join("")}</ul>` : "";
7079
+ const modeledTarget = disclosure.modeledTarget ? `<div><dt>Modeled target</dt><dd>${safe(disclosure.modeledTarget)}</dd></div>` : "";
7080
+ const evidenceRows = evidence ? `<div><dt>Evidence ID</dt><dd>${safe(evidence.evidenceId)}</dd></div><div><dt>Model version</dt><dd>${safe(evidence.modelVersion)}</dd></div><div><dt>Event configuration</dt><dd>${safe(evidence.eventConfigurationId ?? "Not configuration-specific")}</dd></div><div><dt>Approval</dt><dd>${safe(evidence.approvedByRole ?? "No external approval supplied")}</dd></div>` + (evidence.validUntil ? `<div><dt>Valid until</dt><dd>${safe(evidence.validUntil.slice(0, 10))}</dd></div>` : "") : `<div><dt>Evidence ID</dt><dd>None supplied</dd></div>`;
7081
+ const restriction = this.limitedViewLabel(seat.commercial) || this.tf("picker.noAuthoredRestriction", "No organizer-authored restriction");
7082
+ const commercialRows = `<div><dt>View restriction</dt><dd>${safe(restriction)}</dd></div>` + (seat.commercial?.note ? `<div><dt>Organizer note</dt><dd>${safe(seat.commercial.note)}</dd></div>` : "");
7083
+ const shell = document.createElement("div");
7084
+ shell.className = "sl-view3d-passport-shell";
7085
+ shell.innerHTML = `<div class="sl-view3d-passport-scrim" aria-hidden="true"></div><section class="sl-view3d-passport" role="dialog" aria-modal="true" aria-labelledby="sl-view3d-passport-title"><header><div><span>Seat confidence passport</span><strong id="sl-view3d-passport-title">${safe(details?.displayLabel ?? seat.displayLabel ?? seat.label)}</strong></div><button type="button" data-close aria-label="Close seat confidence passport">\xD7</button></header><div class="sl-view3d-passport-summary"><strong>${safe(disclosure.headline)}</strong><span>${safe(disclosure.coverage)} \xB7 ${safe(disclosure.freshness)}</span></div><dl><div><dt>Model status</dt><dd>${safe(disclosure.model)}</dd></div><div><dt>Reality evidence</dt><dd>${safe(disclosure.reality)}</dd></div><div><dt>Source</dt><dd>${safe(disclosure.provenance)}</dd></div>` + commercialRows + modeledTarget + evidenceRows + `</dl>${limitations}<p class="sl-view3d-passport-note">This passport describes supplied evidence and known limits. It does not guarantee that every temporary obstruction or real-world condition is knowable before the event build.</p></section>`;
7086
+ const background = [.../* @__PURE__ */ new Set([
7087
+ ...overlay.children,
7088
+ ...[...this.els.map.children].filter((element) => element !== overlay)
7089
+ ])].filter((element) => element instanceof HTMLElement);
7090
+ const prior = background.map((element) => ({
7091
+ element,
7092
+ inert: element.inert,
7093
+ ariaHidden: element.getAttribute("aria-hidden")
7094
+ }));
7095
+ for (const element of background) {
7096
+ element.inert = true;
7097
+ element.setAttribute("aria-hidden", "true");
7098
+ }
7099
+ overlay.appendChild(shell);
7100
+ overlay.classList.add("has-passport");
7101
+ this.view3dPassportEl = shell;
7102
+ const dialog = shell.querySelector(".sl-view3d-passport");
7103
+ const controls = () => [...dialog.querySelectorAll('button:not(:disabled),[href],[tabindex]:not([tabindex="-1"])')];
7104
+ const onKey = (event) => {
7105
+ if (event.key === "Escape") {
7106
+ event.preventDefault();
7107
+ event.stopPropagation();
7108
+ event.stopImmediatePropagation();
7109
+ this.closeSeatConfidencePassport();
7110
+ return;
7111
+ }
7112
+ if (event.key !== "Tab") return;
7113
+ const focusable = controls();
7114
+ if (!focusable.length) return;
7115
+ const first = focusable[0];
7116
+ const last = focusable[focusable.length - 1];
7117
+ if (event.shiftKey && document.activeElement === first) {
7118
+ event.preventDefault();
7119
+ last.focus();
7120
+ } else if (!event.shiftKey && document.activeElement === last) {
7121
+ event.preventDefault();
7122
+ first.focus();
7123
+ }
7124
+ };
7125
+ window.addEventListener("keydown", onKey, true);
7126
+ this.view3dPassportCleanup = () => {
7127
+ window.removeEventListener("keydown", onKey, true);
7128
+ for (const state of prior) {
7129
+ state.element.inert = state.inert;
7130
+ if (state.ariaHidden === null) state.element.removeAttribute("aria-hidden");
7131
+ else state.element.setAttribute("aria-hidden", state.ariaHidden);
7132
+ }
7133
+ const returnCandidate = returnFocus?.isConnected ? returnFocus : this.confirmEl?.querySelector(".sl-confirm-confidence") ?? this.view3dCompareEl?.querySelector("[data-passport-seat]") ?? null;
7134
+ const returnSurface = returnCandidate?.closest(".sl-confirm,.sl-view3d-compare");
7135
+ if (returnSurface?.isConnected) {
7136
+ returnSurface.inert = false;
7137
+ returnSurface.removeAttribute("aria-hidden");
7138
+ }
7139
+ shell.remove();
7140
+ overlay.classList.remove("has-passport");
7141
+ if (this.view3dPassportEl === shell) this.view3dPassportEl = null;
7142
+ const fallback = returnCandidate ?? this.view3dCompareEl?.querySelector("[data-passport-seat]") ?? this.confirmEl?.querySelector(".sl-confirm-confidence") ?? this.view3dCompareChip?.querySelector(".main");
7143
+ (returnFocus?.isConnected ? returnFocus : fallback)?.focus();
7144
+ };
7145
+ shell.addEventListener("click", (event) => {
7146
+ const target = event.target instanceof HTMLElement ? event.target : null;
7147
+ if (target?.closest("[data-close]") || target?.classList.contains("sl-view3d-passport-scrim")) {
7148
+ this.closeSeatConfidencePassport();
7149
+ }
7150
+ });
7151
+ requestAnimationFrame(() => controls()[0]?.focus());
7152
+ this.emit3dAnalytics("3d_confidence_passport_opened", {
7153
+ seatId: seat.id,
7154
+ evidenceId: evidence?.evidenceId ?? null,
7155
+ eventConfigurationId: evidence?.eventConfigurationId ?? null,
7156
+ modelLevel: evidence?.modelLevel ?? "unverified",
7157
+ realityLevel: evidence?.realityLevel ?? "none"
7158
+ });
7159
+ }
7160
+ closeSeatConfidencePassport(restoreFocus = true) {
7161
+ const cleanup = this.view3dPassportCleanup;
7162
+ this.view3dPassportCleanup = null;
7163
+ if (!cleanup) {
7164
+ this.view3dPassportEl?.remove();
7165
+ this.view3dPassportEl = null;
7166
+ this.view3dEl?.classList.remove("has-passport");
7167
+ return;
7168
+ }
7169
+ if (!restoreFocus) (document.activeElement instanceof HTMLElement ? document.activeElement : null)?.blur();
7170
+ cleanup();
7171
+ if (!restoreFocus) this.root?.focus({ preventScroll: true });
7172
+ }
7173
+ openView3dComparison() {
7174
+ const overlay = this.view3dEl;
7175
+ if (!overlay || this.view3dCompareSeatIds.length < 2) return;
7176
+ this.closeView3dComparison(false);
7177
+ const snapshots = this.view3dCompareSeatIds.map((seatId) => this.view3dComparisonSnapshot(seatId)).filter((value) => !!value);
7178
+ if (snapshots.length < 2) {
7179
+ this.clearView3dComparison();
7180
+ return;
7181
+ }
7182
+ const safe = (value) => this.escCx(value);
7183
+ const shell = document.createElement("div");
7184
+ shell.className = "sl-view3d-compare-shell";
7185
+ const cards = snapshots.map((snapshot, index) => `<article><span>Seat ${index === 0 ? "A" : "B"}</span><strong>${safe(snapshot.label)}</strong><small>Section ${safe(snapshot.section)} \xB7 Row ${safe(snapshot.row)}</small><dl><div><dt>Current price</dt><dd>${safe(snapshot.price)}</dd></div><div><dt>Ticket type</dt><dd>${safe(snapshot.category)}</dd></div><div><dt>Availability</dt><dd>${safe(snapshot.availability)}</dd></div><div><dt>View source</dt><dd>${safe(snapshot.viewSource)}</dd></div><div><dt>View restriction</dt><dd>${safe(snapshot.limited)}</dd></div><div><dt>Seat confidence</dt><dd>${safe(snapshot.confidence.headline)}</dd></div><div><dt>Reality check</dt><dd>${safe(snapshot.confidence.reality)}</dd></div><div><dt>Accessibility</dt><dd>${safe(snapshot.accessibility)}</dd></div></dl><div class="sl-view3d-compare-actions"><button type="button" data-passport-seat="${safe(snapshot.seat.id)}">Passport</button><button type="button" data-view-seat="${safe(snapshot.seat.id)}">View seat</button><button type="button" class="select" data-select-seat="${safe(snapshot.seat.id)}"${snapshot.selectable ? "" : " disabled"}>Select seat</button></div></article>`).join("");
7186
+ shell.innerHTML = `<div class="sl-view3d-compare-scrim" aria-hidden="true"></div><section class="sl-view3d-compare" role="dialog" aria-modal="true" aria-labelledby="sl-view3d-compare-title"><header><div><span>Seat inspection</span><strong id="sl-view3d-compare-title">Compare disclosed attributes</strong></div><button type="button" data-close aria-label="Close seat comparison">\xD7</button></header><p class="sl-view3d-compare-note">Current price and availability come from this picker. Modeled views are chart-derived unless organizer media is labeled; SeatLayer does not invent why a seat has its price.</p><div class="sl-view3d-compare-grid">${cards}</div></section>`;
7187
+ const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
7188
+ const background = [...overlay.children].filter((element) => element instanceof HTMLElement);
7189
+ const prior = background.map((element) => ({
7190
+ element,
7191
+ inert: element.inert,
7192
+ ariaHidden: element.getAttribute("aria-hidden")
7193
+ }));
7194
+ for (const element of background) {
7195
+ element.inert = true;
7196
+ element.setAttribute("aria-hidden", "true");
7197
+ }
7198
+ overlay.appendChild(shell);
7199
+ overlay.classList.add("has-comparison");
7200
+ this.view3dCompareEl = shell;
7201
+ const dialog = shell.querySelector(".sl-view3d-compare");
7202
+ const controls = () => [...dialog.querySelectorAll('button:not(:disabled),[href],[tabindex]:not([tabindex="-1"])')];
7203
+ const onKey = (event) => {
7204
+ if (event.key === "Escape") {
7205
+ event.preventDefault();
7206
+ this.closeView3dComparison();
7207
+ return;
7208
+ }
7209
+ if (event.key !== "Tab") return;
7210
+ const focusable = controls();
7211
+ if (!focusable.length) return;
7212
+ const first = focusable[0];
7213
+ const last = focusable[focusable.length - 1];
7214
+ if (event.shiftKey && document.activeElement === first) {
7215
+ event.preventDefault();
7216
+ last.focus();
7217
+ } else if (!event.shiftKey && document.activeElement === last) {
7218
+ event.preventDefault();
7219
+ first.focus();
7220
+ }
7221
+ };
7222
+ window.addEventListener("keydown", onKey);
7223
+ this.view3dCompareCleanup = () => {
7224
+ window.removeEventListener("keydown", onKey);
7225
+ for (const state of prior) {
7226
+ state.element.inert = state.inert;
7227
+ if (state.ariaHidden === null) state.element.removeAttribute("aria-hidden");
7228
+ else state.element.setAttribute("aria-hidden", state.ariaHidden);
7229
+ }
7230
+ shell.remove();
7231
+ overlay.classList.remove("has-comparison");
7232
+ if (this.view3dCompareEl === shell) this.view3dCompareEl = null;
7233
+ if (previousFocus?.isConnected) previousFocus.focus();
7234
+ else this.view3dCompareChip?.querySelector(".main")?.focus();
7235
+ };
7236
+ shell.querySelector("[data-close]")?.addEventListener("click", () => this.closeView3dComparison());
7237
+ shell.querySelectorAll("[data-passport-seat]").forEach((button) => button.addEventListener("click", () => {
7238
+ const seatId = button.dataset.passportSeat;
7239
+ const seat = seatId ? this.allSeats().find((candidate) => candidate.id === seatId) : void 0;
7240
+ if (seat) this.openSeatConfidencePassport(seat, button);
7241
+ }));
7242
+ shell.querySelectorAll("[data-view-seat]").forEach((button) => button.addEventListener("click", () => {
7243
+ const seatId = button.dataset.viewSeat;
7244
+ this.closeView3dComparison(false);
7245
+ if (seatId) void this.view3dHandle?.flyToSeat(seatId);
7246
+ }));
7247
+ shell.querySelectorAll("[data-select-seat]").forEach((button) => button.addEventListener("click", () => {
7248
+ const seatId = button.dataset.selectSeat;
7249
+ if (seatId) this.selectComparedSeat(seatId);
7250
+ }));
7251
+ requestAnimationFrame(() => controls()[0]?.focus());
7252
+ this.emit3dAnalytics("3d_comparison_opened", { seatIds: this.view3dCompareSeatIds.slice() });
7253
+ }
7254
+ closeView3dComparison(restoreFocus = true) {
7255
+ const cleanup = this.view3dCompareCleanup;
7256
+ this.view3dCompareCleanup = null;
7257
+ if (!cleanup) {
7258
+ this.view3dCompareEl?.remove();
7259
+ this.view3dCompareEl = null;
7260
+ this.view3dEl?.classList.remove("has-comparison");
7261
+ return;
7262
+ }
7263
+ if (!restoreFocus) {
7264
+ const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
7265
+ active?.blur();
7266
+ }
7267
+ cleanup();
7268
+ if (!restoreFocus) this.root?.focus({ preventScroll: true });
7269
+ }
7270
+ selectComparedSeat(seatId) {
7271
+ if (this.salesClosed) {
7272
+ this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
7273
+ return;
7274
+ }
7275
+ const seat = this.allSeats().find((candidate) => candidate.id === seatId);
7276
+ if (!seat) return;
7277
+ this.closeView3dComparison(false);
7278
+ const added = this.controller.select([seatId]);
7279
+ if (!added.length) {
7280
+ this.syncSelectionTo3d();
7281
+ this.toast(this.tf("picker.seatNoLongerAvailable", "That seat is no longer available."), "warning");
7282
+ return;
7283
+ }
7284
+ this.syncSelectionTo3d();
7285
+ this.showConfirm(seat);
7286
+ this.emit3dAnalytics("3d_comparison_selected", { seatId });
6492
7287
  }
6493
7288
  /**
6494
7289
  * The venue-navigation rail inside 3D: levels and areas.
@@ -6517,6 +7312,25 @@ var SeatPicker = class _SeatPicker {
6517
7312
  if (!wantFloors && !wantZones && !wantSections && !wantLocator) return;
6518
7313
  const nav = document.createElement("div");
6519
7314
  nav.className = "sl-view3d-nav";
7315
+ const finderToggle = document.createElement("button");
7316
+ finderToggle.type = "button";
7317
+ finderToggle.className = "sl-view3d-nav-toggle";
7318
+ finderToggle.setAttribute("aria-expanded", "false");
7319
+ const setFinderOpen = (open) => {
7320
+ nav.classList.toggle("is-open", open);
7321
+ finderToggle.setAttribute("aria-expanded", String(open));
7322
+ finderToggle.textContent = open ? "Close seat finder" : "Find a seat";
7323
+ };
7324
+ finderToggle.addEventListener("click", () => setFinderOpen(!nav.classList.contains("is-open")));
7325
+ nav.addEventListener("keydown", (event) => {
7326
+ if (event.key !== "Escape" || !nav.classList.contains("is-open")) return;
7327
+ event.preventDefault();
7328
+ event.stopPropagation();
7329
+ setFinderOpen(false);
7330
+ finderToggle.focus();
7331
+ });
7332
+ setFinderOpen(false);
7333
+ nav.appendChild(finderToggle);
6520
7334
  if (wantFloors) {
6521
7335
  const row = document.createElement("div");
6522
7336
  row.setAttribute("role", "group");
@@ -6534,7 +7348,10 @@ var SeatPicker = class _SeatPicker {
6534
7348
  b.textContent = label;
6535
7349
  b.dataset.floor = index === null ? "" : String(index);
6536
7350
  b.setAttribute("aria-pressed", String(index === null));
6537
- b.addEventListener("click", () => select(index));
7351
+ b.addEventListener("click", () => {
7352
+ select(index);
7353
+ setFinderOpen(false);
7354
+ });
6538
7355
  pills.push(b);
6539
7356
  row.appendChild(b);
6540
7357
  };
@@ -6573,7 +7390,10 @@ var SeatPicker = class _SeatPicker {
6573
7390
  const b = document.createElement("button");
6574
7391
  b.type = "button";
6575
7392
  b.textContent = e.label;
6576
- b.addEventListener("click", e.go);
7393
+ b.addEventListener("click", () => {
7394
+ e.go();
7395
+ setFinderOpen(false);
7396
+ });
6577
7397
  row.appendChild(b);
6578
7398
  }
6579
7399
  }
@@ -6592,7 +7412,7 @@ var SeatPicker = class _SeatPicker {
6592
7412
  seatSelect.setAttribute("aria-label", "Choose seat in 3D");
6593
7413
  const view = document.createElement("button");
6594
7414
  view.type = "button";
6595
- view.textContent = "View seat";
7415
+ view.textContent = "Inspect seat";
6596
7416
  view.disabled = true;
6597
7417
  const fill = (select, placeholder, entries) => {
6598
7418
  select.replaceChildren();
@@ -6648,11 +7468,13 @@ var SeatPicker = class _SeatPicker {
6648
7468
  seatSelect.addEventListener("change", () => {
6649
7469
  const seatId = seatSelect.value;
6650
7470
  view.disabled = !seatId;
6651
- handle.setSelection(seatId ? [seatId] : []);
6652
7471
  });
6653
7472
  view.addEventListener("click", () => {
6654
7473
  const seatId = seatSelect.value;
6655
- if (seatId) void handle.flyToSeat(seatId);
7474
+ if (seatId) {
7475
+ setFinderOpen(false);
7476
+ this.onView3dSeatPick(seatId);
7477
+ }
6656
7478
  });
6657
7479
  locator.append(sectionSelect, rowSelect, seatSelect, view);
6658
7480
  nav.appendChild(locator);
@@ -6664,6 +7486,7 @@ var SeatPicker = class _SeatPicker {
6664
7486
  const doc = this.controller.doc;
6665
7487
  if (!doc) return;
6666
7488
  this.buyerView = "venue3d";
7489
+ this.view3dTargetSeatId = null;
6667
7490
  this.opts.onBuyerViewChange?.({ view: "venue3d", ...flySeatId ? { seatId: flySeatId } : {} });
6668
7491
  this.root?.setAttribute("data-view3d", "on");
6669
7492
  this.dismissConfirm();
@@ -6676,7 +7499,22 @@ var SeatPicker = class _SeatPicker {
6676
7499
  back.type = "button";
6677
7500
  back.className = "sl-view3d-back";
6678
7501
  back.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg><span>${this.tf("picker.backToMap", "Back to map")}</span>`;
6679
- back.addEventListener("click", () => this.exit3d());
7502
+ const backLabel = back.querySelector("span");
7503
+ const setJourneyTarget = (seatId) => {
7504
+ this.view3dTargetSeatId = seatId;
7505
+ const atSeat = !!seatId;
7506
+ overlay.classList.toggle("is-seat-focused", atSeat);
7507
+ const label = atSeat ? this.tf("picker.backToVenue", "Back to venue") : this.tf("picker.backToMap", "Back to map");
7508
+ if (backLabel) backLabel.textContent = label;
7509
+ back.setAttribute("aria-label", label);
7510
+ };
7511
+ back.addEventListener("click", () => {
7512
+ if (this.view3dTargetSeatId && this.view3dHandle) {
7513
+ this.view3dHandle.focusOverview();
7514
+ return;
7515
+ }
7516
+ this.exit3d();
7517
+ });
6680
7518
  overlay.appendChild(back);
6681
7519
  const fullscreen = document.createElement("button");
6682
7520
  fullscreen.type = "button";
@@ -6694,6 +7532,7 @@ var SeatPicker = class _SeatPicker {
6694
7532
  overlay.appendChild(loading);
6695
7533
  this.els.map.appendChild(overlay);
6696
7534
  this.view3dEl = overlay;
7535
+ this.syncView3dCompareChip();
6697
7536
  requestAnimationFrame(() => {
6698
7537
  overlay.style.opacity = "1";
6699
7538
  });
@@ -6709,7 +7548,13 @@ var SeatPicker = class _SeatPicker {
6709
7548
  // phone. The bounded portrait fit was validated against every UX-lab
6710
7549
  // fixture; keep editor previews strict while making buyer seats legible.
6711
7550
  portraitOverviewCrop: true,
7551
+ // Premium buyer mode lands at the modeled seated-eye and locks the
7552
+ // venue orbit there. Explicit look-around rotates at that same origin;
7553
+ // zooming cannot escape through the shell or reveal backstage geometry.
7554
+ arriveAtSeatEye: true,
7555
+ seatViewActionLabel: (seatId) => this.allSeats().find((seat) => seat.id === seatId)?.viewUrl ? this.tf("picker.openAuthored360", "Open venue 360\xB0") : this.tf("picker.lookAroundLive3d", "Look around in live 3D"),
6712
7556
  onSeatPick: (id) => this.onView3dSeatPick(id),
7557
+ onSeatInspect: (id) => this.onView3dSeatPick(id),
6713
7558
  onSectionFocusChange: (sectionId) => {
6714
7559
  const select = overlay.querySelector(
6715
7560
  'select[aria-label="Choose section in 3D"]'
@@ -6721,6 +7566,7 @@ var SeatPicker = class _SeatPicker {
6721
7566
  },
6722
7567
  onViewTargetChange: (seatId) => {
6723
7568
  overlay.querySelector(".sl-view3d-nav")?.classList.toggle("is-seat-focused", !!seatId);
7569
+ setJourneyTarget(seatId);
6724
7570
  this.opts.onBuyerViewChange?.({
6725
7571
  view: "venue3d",
6726
7572
  ...seatId ? { seatId } : {}
@@ -6761,8 +7607,13 @@ var SeatPicker = class _SeatPicker {
6761
7607
  if (this.buyerView !== "venue3d" && !this.view3dEl) return;
6762
7608
  this.view3dGen++;
6763
7609
  this.buyerView = "map";
7610
+ this.view3dTargetSeatId = null;
6764
7611
  this.opts.onBuyerViewChange?.({ view: "map" });
6765
7612
  this.root?.removeAttribute("data-view3d");
7613
+ this.closeSeatConfidencePassport(false);
7614
+ this.closeView3dComparison(false);
7615
+ this.view3dCompareChip?.remove();
7616
+ this.view3dCompareChip = null;
6766
7617
  try {
6767
7618
  this.view3dHandle?.dispose();
6768
7619
  } catch {
@@ -6802,6 +7653,7 @@ var SeatPicker = class _SeatPicker {
6802
7653
  const button = this.els.tray?.querySelector(".sl-ba-go");
6803
7654
  if (button) {
6804
7655
  button.disabled = true;
7656
+ button.classList.add("sl-busy");
6805
7657
  button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
6806
7658
  }
6807
7659
  try {
@@ -7048,7 +7900,8 @@ var SeatPicker = class _SeatPicker {
7048
7900
  if (this.modalScrim) {
7049
7901
  this.modalScrim.remove();
7050
7902
  this.modalScrim = null;
7051
- this.prevFocus?.focus?.();
7903
+ if (this.prevFocus?.isConnected) this.prevFocus.focus({ preventScroll: true });
7904
+ this.prevFocus = null;
7052
7905
  }
7053
7906
  }
7054
7907
  };
@@ -7139,46 +7992,17 @@ function attachPickerFrame(iframe, opts = {}) {
7139
7992
  };
7140
7993
  }
7141
7994
  export {
7142
- ACCESS_LINK_DEFAULTS,
7143
7995
  ApiError,
7144
7996
  BuyerAccessContext,
7145
7997
  BuyerAccessUnavailableError,
7146
7998
  BuyerRealtimeClient,
7147
- ChannelsMode,
7148
7999
  EmbeddedDesigner,
7149
- ManageApi,
7150
- ManageApiError,
7151
- PUBLIC_CHANNEL_ID,
7152
- PUBLIC_CHANNEL_NAME,
7153
- SeatManager,
7154
8000
  SeatPicker,
7155
8001
  SeatingChart,
7156
- accessIntentDescription,
7157
- accessIntentLabel,
7158
- accessLine,
7159
- accessLinkBadge,
7160
- accessLinkErrorCopy,
7161
- accessLinkIsLive,
7162
- accessLinkPolicyLines,
7163
8002
  attachPickerFrame,
7164
- bucketRows,
7165
- bucketRowsHtml,
7166
8003
  createBuyerAccessContext,
7167
8004
  createControllerSink,
7168
- dropReviewRows,
7169
- intentForbidsCopy,
7170
- intentSwitchBlockedCopy,
7171
- isPublicChannelId,
7172
- markerLetter,
7173
- markerOf,
7174
- mutationCount,
7175
- needsMoveConfirmation,
7176
8005
  parseTicketOfferAvailability,
7177
- planAssignment,
7178
- retryAfterCopy,
7179
- selectionSources,
7180
- stateBadge,
7181
- suggestMarker,
7182
8006
  ticketOfferPrices
7183
8007
  };
7184
8008
  //# sourceMappingURL=index.js.map