@seatlayer/js 0.17.0 → 0.18.1

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.cjs CHANGED
@@ -358,16 +358,46 @@ var TYPES = /* @__PURE__ */ new Set([
358
358
  "seatlayer.designer.close",
359
359
  "seatlayer.designer.error"
360
360
  ]);
361
+ var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
361
362
  function resolveContainer2(container) {
362
363
  if (typeof container !== "string") return container;
363
364
  const element = document.querySelector(container);
364
365
  if (!element) throw new Error(`EmbeddedDesigner container not found: ${container}`);
365
366
  return element;
366
367
  }
368
+ function causeFromCode(code) {
369
+ const value = (code ?? "").toLowerCase();
370
+ if (value.includes("expire") || value.includes("revoke") || value === "401") return "expired";
371
+ if (value.includes("mismatch")) return "mismatch";
372
+ if (value.includes("timeout")) return "timeout";
373
+ return "load";
374
+ }
375
+ var ERROR_COPY = {
376
+ expired: {
377
+ title: "This design session expired",
378
+ body: "For your security, editing sessions are short-lived. Start a fresh one to keep designing."
379
+ },
380
+ mismatch: {
381
+ title: "This editor doesn't match this chart",
382
+ body: "The session that loaded belongs to a different chart or workspace. Reopen the designer to continue."
383
+ },
384
+ timeout: {
385
+ title: "The designer is taking too long",
386
+ body: "It did not finish loading in time. This is usually a slow connection \u2014 try again."
387
+ },
388
+ load: {
389
+ title: "We couldn't load the designer",
390
+ body: "Something went wrong while opening the editor. Please try again."
391
+ }
392
+ };
367
393
  var EmbeddedDesigner = class {
368
394
  constructor(options) {
369
395
  this.frame = null;
370
396
  this.designerOrigin = "";
397
+ this.overlay = null;
398
+ this.timeoutTimer = null;
399
+ this.phase = "loading";
400
+ this.restoreContainerPosition = null;
371
401
  this.handleMessage = (event) => {
372
402
  if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
373
403
  if (!event.data || typeof event.data !== "object") return;
@@ -382,10 +412,15 @@ var EmbeddedDesigner = class {
382
412
  message: typeof data.message === "string" ? data.message : void 0,
383
413
  meta: data.meta
384
414
  };
385
- if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId) return;
386
- if (this.options.expectedWorkspaceId && message.workspaceId && message.workspaceId !== this.options.expectedWorkspaceId) return;
415
+ if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId || this.options.expectedWorkspaceId && message.workspaceId && message.workspaceId !== this.options.expectedWorkspaceId) {
416
+ this.showError("mismatch");
417
+ return;
418
+ }
387
419
  switch (message.type) {
388
420
  case "seatlayer.designer.ready":
421
+ this.phase = "ready";
422
+ this.clearTimeoutTimer();
423
+ this.removeOverlay();
389
424
  this.options.onReady?.(message);
390
425
  break;
391
426
  case "seatlayer.designer.saved":
@@ -398,6 +433,7 @@ var EmbeddedDesigner = class {
398
433
  this.options.onClose?.(message);
399
434
  break;
400
435
  case "seatlayer.designer.error":
436
+ this.showError(causeFromCode(message.code));
401
437
  this.options.onError?.(message);
402
438
  break;
403
439
  }
@@ -421,9 +457,21 @@ var EmbeddedDesigner = class {
421
457
  frame.style.border = "0";
422
458
  Object.assign(frame.style, this.options.style);
423
459
  if (this.options.className) frame.className = this.options.className;
460
+ const container = resolveContainer2(this.options.container);
424
461
  window.addEventListener("message", this.handleMessage);
425
- resolveContainer2(this.options.container).append(frame);
462
+ container.append(frame);
426
463
  this.frame = frame;
464
+ this.phase = "loading";
465
+ if (this.loadingStateEnabled()) {
466
+ this.ensureContainerPositioned(container);
467
+ this.renderOverlay(container, "loading");
468
+ const timeout = this.options.loadingTimeoutMs ?? DEFAULT_LOADING_TIMEOUT_MS;
469
+ if (timeout > 0 && Number.isFinite(timeout)) {
470
+ this.timeoutTimer = setTimeout(() => {
471
+ if (this.phase === "loading") this.showError("timeout");
472
+ }, timeout);
473
+ }
474
+ }
427
475
  return frame;
428
476
  }
429
477
  /** Replace the iframe instead of assigning a new fragment to an existing one. */
@@ -436,9 +484,208 @@ var EmbeddedDesigner = class {
436
484
  }
437
485
  destroy() {
438
486
  window.removeEventListener("message", this.handleMessage);
487
+ this.clearTimeoutTimer();
488
+ this.removeOverlay();
489
+ this.restoreContainerStyle();
439
490
  this.frame?.remove();
440
491
  this.frame = null;
441
492
  this.designerOrigin = "";
493
+ this.phase = "loading";
494
+ }
495
+ loadingStateEnabled() {
496
+ return this.options.showLoadingState !== false;
497
+ }
498
+ clearTimeoutTimer() {
499
+ if (this.timeoutTimer !== null) {
500
+ clearTimeout(this.timeoutTimer);
501
+ this.timeoutTimer = null;
502
+ }
503
+ }
504
+ ensureContainerPositioned(container) {
505
+ const position = getComputedStyle(container).position;
506
+ if (position === "static") {
507
+ this.restoreContainerPosition = container.style.position;
508
+ container.style.position = "relative";
509
+ }
510
+ }
511
+ restoreContainerStyle() {
512
+ if (this.restoreContainerPosition === null) return;
513
+ try {
514
+ resolveContainer2(this.options.container).style.position = this.restoreContainerPosition;
515
+ } catch {
516
+ }
517
+ this.restoreContainerPosition = null;
518
+ }
519
+ removeOverlay() {
520
+ this.overlay?.remove();
521
+ this.overlay = null;
522
+ }
523
+ showError(cause) {
524
+ this.phase = "error";
525
+ this.clearTimeoutTimer();
526
+ if (!this.loadingStateEnabled()) return;
527
+ let container;
528
+ try {
529
+ container = resolveContainer2(this.options.container);
530
+ } catch {
531
+ return;
532
+ }
533
+ this.renderOverlay(container, "error", cause);
534
+ }
535
+ handleTryAgain() {
536
+ if (this.options.onRequestRelaunch) {
537
+ this.options.onRequestRelaunch();
538
+ return;
539
+ }
540
+ this.mount();
541
+ }
542
+ /**
543
+ * Build (or rebuild) the overlay for the given phase. A single overlay element
544
+ * is reused so we never stack stale skeletons or cards.
545
+ */
546
+ renderOverlay(container, phase, cause) {
547
+ this.removeOverlay();
548
+ const overlay = document.createElement("div");
549
+ overlay.setAttribute("data-seatlayer-designer-overlay", phase);
550
+ overlay.setAttribute("role", phase === "error" ? "alert" : "status");
551
+ overlay.setAttribute("aria-live", "polite");
552
+ Object.assign(overlay.style, {
553
+ position: "absolute",
554
+ inset: "0",
555
+ display: "flex",
556
+ alignItems: "center",
557
+ justifyContent: "center",
558
+ background: "#101625",
559
+ color: "#e6ebf5",
560
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
561
+ zIndex: "2",
562
+ overflow: "hidden"
563
+ });
564
+ if (phase === "loading") this.buildSkeleton(overlay);
565
+ else this.buildErrorCard(overlay, cause ?? "load");
566
+ container.append(overlay);
567
+ this.overlay = overlay;
568
+ }
569
+ buildSkeleton(overlay) {
570
+ const style = document.createElement("style");
571
+ style.textContent = `
572
+ @media (prefers-reduced-motion: no-preference) {
573
+ @keyframes seatlayer-designer-shimmer {
574
+ 0% { background-position: -320px 0; }
575
+ 100% { background-position: 320px 0; }
576
+ }
577
+ [data-seatlayer-designer-overlay="loading"] .sl-shimmer {
578
+ animation: seatlayer-designer-shimmer 1.25s ease-in-out infinite;
579
+ background-size: 640px 100%;
580
+ }
581
+ }`;
582
+ overlay.append(style);
583
+ const shimmer = "linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.10) 37%, rgba(255,255,255,0.04) 63%)";
584
+ const scaffold = document.createElement("div");
585
+ Object.assign(scaffold.style, {
586
+ position: "absolute",
587
+ inset: "0",
588
+ display: "flex",
589
+ flexDirection: "column",
590
+ padding: "16px",
591
+ gap: "14px",
592
+ opacity: "0.9"
593
+ });
594
+ const bar = (styles) => {
595
+ const node = document.createElement("div");
596
+ node.className = "sl-shimmer";
597
+ Object.assign(node.style, {
598
+ background: shimmer,
599
+ borderRadius: "8px"
600
+ });
601
+ Object.assign(node.style, styles);
602
+ return node;
603
+ };
604
+ scaffold.append(bar({ height: "40px", width: "100%", flex: "0 0 auto" }));
605
+ const body = document.createElement("div");
606
+ Object.assign(body.style, {
607
+ display: "flex",
608
+ gap: "14px",
609
+ flex: "1 1 auto",
610
+ minHeight: "0"
611
+ });
612
+ body.append(bar({ width: "220px", height: "100%", flex: "0 0 auto" }));
613
+ body.append(bar({ flex: "1 1 auto", height: "100%" }));
614
+ scaffold.append(body);
615
+ overlay.append(scaffold);
616
+ const caption = document.createElement("div");
617
+ Object.assign(caption.style, {
618
+ position: "relative",
619
+ zIndex: "1",
620
+ display: "flex",
621
+ alignItems: "center",
622
+ gap: "10px",
623
+ padding: "10px 16px",
624
+ borderRadius: "999px",
625
+ background: "rgba(16, 22, 37, 0.72)",
626
+ fontSize: "13px",
627
+ fontWeight: "500",
628
+ letterSpacing: "0.01em"
629
+ });
630
+ const dot = document.createElement("span");
631
+ dot.className = "sl-shimmer";
632
+ Object.assign(dot.style, {
633
+ width: "9px",
634
+ height: "9px",
635
+ borderRadius: "50%",
636
+ background: shimmer,
637
+ flex: "0 0 auto"
638
+ });
639
+ caption.append(dot);
640
+ caption.append(document.createTextNode("Loading designer\u2026"));
641
+ overlay.append(caption);
642
+ }
643
+ buildErrorCard(overlay, cause) {
644
+ const copy = ERROR_COPY[cause];
645
+ const card = document.createElement("div");
646
+ Object.assign(card.style, {
647
+ maxWidth: "420px",
648
+ margin: "0 24px",
649
+ padding: "28px",
650
+ textAlign: "center",
651
+ background: "rgba(255, 255, 255, 0.03)",
652
+ border: "1px solid rgba(255, 255, 255, 0.08)",
653
+ borderRadius: "16px",
654
+ boxShadow: "0 12px 40px rgba(0, 0, 0, 0.35)"
655
+ });
656
+ const heading = document.createElement("h2");
657
+ heading.textContent = copy.title;
658
+ Object.assign(heading.style, {
659
+ margin: "0 0 8px",
660
+ fontSize: "17px",
661
+ fontWeight: "600",
662
+ color: "#f4f7ff"
663
+ });
664
+ const body = document.createElement("p");
665
+ body.textContent = copy.body;
666
+ Object.assign(body.style, {
667
+ margin: "0 0 20px",
668
+ fontSize: "13.5px",
669
+ lineHeight: "1.5",
670
+ color: "#aab4c8"
671
+ });
672
+ const button = document.createElement("button");
673
+ button.type = "button";
674
+ button.textContent = "Try again";
675
+ Object.assign(button.style, {
676
+ appearance: "none",
677
+ cursor: "pointer",
678
+ border: "0",
679
+ borderRadius: "10px",
680
+ padding: "10px 22px",
681
+ fontSize: "14px",
682
+ fontWeight: "600",
683
+ color: "#101625",
684
+ background: "#7aa2ff"
685
+ });
686
+ button.addEventListener("click", () => this.handleTryAgain());
687
+ card.append(heading, body, button);
688
+ overlay.append(card);
442
689
  }
443
690
  };
444
691
 
@@ -603,26 +850,43 @@ var CSS = `
603
850
  .sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
604
851
  overscroll-behavior:contain;scrollbar-gutter:stable}
605
852
  .sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
606
- .sl-chip{position:relative;display:grid;grid-template-columns:24px minmax(0,1fr) auto 30px;align-items:center;gap:8px;
607
- min-height:53px;padding:7px 7px 7px 9px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);
853
+ .sl-chip{position:relative;display:grid;grid-template-columns:minmax(0,1fr) 34px;align-items:stretch;
854
+ flex:none;min-height:53px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);overflow:hidden;
608
855
  background:var(--sl-surface);font-size:13px;transform-origin:center;transition:border-color .15s,background .15s}
609
856
  .sl-chip:hover{border-color:color-mix(in srgb,var(--sl-accent) 38%,var(--sl-line))}
610
857
  .sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
611
858
  .sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
612
859
  .sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
613
860
  box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
614
- .sl-ticket-state{width:23px;height:23px;border-radius:999px;display:flex;align-items:center;justify-content:center;
861
+ .sl-ticket-state{width:17px;height:17px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
615
862
  background:var(--sl-accent);color:var(--sl-accent-ink)}
616
863
  .sl-ticket-state.held{background:color-mix(in srgb,var(--sl-accent) 18%,var(--sl-surface));color:var(--sl-accent)}
617
- .sl-ticket-state svg{width:13px;height:13px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
618
- .sl-chip-main{min-width:0}
619
- .sl-chip b{display:block;font-weight:800;min-width:max-content;white-space:nowrap}
620
- .sl-chip-sub{display:flex;align-items:center;gap:5px;min-width:0;margin-top:2px}
864
+ .sl-ticket-state svg{width:10px;height:10px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
865
+ .sl-chip-main{min-width:0;padding:8px 10px 8px 11px;display:flex;flex-direction:column;justify-content:center;gap:5px}
866
+ .sl-chip-id{display:flex;gap:12px;min-width:0}
867
+ .sl-chip-id .fld{min-width:0}
868
+ .sl-chip-id .fld.sec{flex:1}
869
+ .sl-chip-id .fld.mid{flex:none;text-align:center}
870
+ .sl-chip-eb{display:block;font-size:8px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);margin-bottom:1px}
871
+ .sl-chip-id .val{display:block;font-weight:600;font-size:13px;line-height:1.25;white-space:nowrap}
872
+ .sl-chip-id .fld.sec .val{overflow:hidden;text-overflow:ellipsis}
873
+ .sl-chip-sub{display:flex;align-items:center;gap:6px;min-width:0}
621
874
  .sl-chip .cat{color:var(--sl-muted);font-size:10.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
622
875
  .sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums;flex:none;white-space:nowrap}
623
- .sl-chip .rm{width:29px;height:29px;border-radius:8px;flex:none;display:flex;align-items:center;justify-content:center;color:var(--sl-muted)}
624
- .sl-chip .rm:hover,.sl-chip .rm:focus-visible{color:var(--sl-accent);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
876
+ .sl-chip-rail{display:flex;flex-direction:column;border-left:1px solid var(--sl-line)}
877
+ .sl-chip .rm,.sl-chip .view{flex:1;min-height:26px;border-radius:0;display:flex;align-items:center;justify-content:center;
878
+ color:var(--sl-muted);transition:color .15s,background .15s}
879
+ .sl-chip .view{border-top:1px solid var(--sl-line)}
880
+ .sl-chip .rm:hover,.sl-chip .rm:focus-visible{color:#e5484d;background:color-mix(in srgb,#e5484d 9%,transparent)}
881
+ .sl-chip .view:hover,.sl-chip .view:focus-visible{color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
625
882
  .sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
883
+ .sl-chip .view svg{width:13px;height:13px;stroke:currentColor;stroke-width:1.8;fill:none}
884
+ /* live-activity strip \u2014 narrates WS availability deltas (social proof + urgency) */
885
+ .sl-live{display:flex;align-items:center;gap:7px;margin:10px 14px 0;padding:7px 9px;flex:none;
886
+ border:1px solid var(--sl-line);border-radius:8px;background:color-mix(in srgb,var(--sl-accent) 4%,var(--sl-surface));
887
+ font-size:11px;color:var(--sl-muted)}
888
+ .sl-live .dot{width:6px;height:6px;border-radius:999px;background:#22a06b;box-shadow:0 0 6px rgba(34,160,107,.75);flex:none}
889
+ .sl-live span:last-child{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
626
890
 
627
891
  /* GA rows */
628
892
  .sl-ga{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px dashed var(--sl-line);border-radius:var(--sl-r-sm)}
@@ -698,6 +962,8 @@ var CSS = `
698
962
 
699
963
  /* zoom column (flows within the bottom-right region) */
700
964
  .sl-zoom{display:flex;flex-direction:column;gap:6px}
965
+ /* CSS-fallback full screen (iOS Safari has no element fullscreen API) */
966
+ .sl-picker.sl-fs{position:fixed;inset:0;z-index:2147483000;width:auto;height:auto;max-height:none;border-radius:0}
701
967
  .sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
702
968
  color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
703
969
  .sl-zoom button:hover{border-color:var(--sl-muted)}
@@ -756,6 +1022,39 @@ var CSS = `
756
1022
  .sl-booked.on .sl-booked-title{animation-delay:.22s}
757
1023
  .sl-booked.on .sl-booked-sub{animation-delay:.3s}
758
1024
 
1025
+ /* sold-out overlay \u2014 every SEATED category's live availability is 0. Centered
1026
+ over the map; a stub (disabled) "Join waitlist" button, exactly like the page.
1027
+ Suppressed when GA areas exist (GA capacity isn't seat-counted). Clears live
1028
+ the moment WS frees a seat up. */
1029
+ .sl-soldout{position:absolute;inset:0;z-index:10;display:none;flex-direction:column;align-items:center;
1030
+ justify-content:center;text-align:center;gap:8px;padding:24px;
1031
+ background:color-mix(in srgb,var(--sl-bg) 82%,transparent);backdrop-filter:blur(4px)}
1032
+ .sl-soldout.on{display:flex}
1033
+ .sl-soldout-eyebrow{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--sl-accent);font-weight:800}
1034
+ .sl-soldout-title{font-size:32px;font-weight:800;color:var(--sl-text);line-height:1.05}
1035
+ .sl-soldout-copy{max-width:360px;font-size:13px;color:var(--sl-muted);line-height:1.5}
1036
+ .sl-picker .sl-soldout-btn{margin-top:10px;min-height:40px;padding:10px 18px;border-radius:var(--sl-r-sm);
1037
+ background:var(--sl-surface);color:var(--sl-muted);border:1px solid var(--sl-line);font-weight:800;font-size:13px;
1038
+ cursor:not-allowed;opacity:.85}
1039
+
1040
+ /* sales-closed pill (header) \u2014 persistent read-only state when the event's sales
1041
+ window is closed at load or closes live mid-session. Neutral (not accent) so it
1042
+ reads as "unavailable", distinct from the accent hold pill next to it. */
1043
+ .sl-closed-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
1044
+ background:color-mix(in srgb,var(--sl-text) 12%,var(--sl-surface));color:var(--sl-text);
1045
+ font-weight:700;font-size:12px;white-space:nowrap}
1046
+ .sl-closed-pill.on{display:inline-flex}
1047
+ .sl-closed-pill svg{width:13px;height:13px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
1048
+
1049
+ /* "Powered by SeatLayer" attribution badge (side-panel foot) \u2014 the small gold
1050
+ rounded logo mark + wordmark. Hidden when the host opts out or the org's paid
1051
+ theme sets hideBadge. */
1052
+ .sl-powered{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:10px;
1053
+ font-size:11px;letter-spacing:.03em;color:var(--sl-muted)}
1054
+ .sl-powered-mark{width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;
1055
+ background:var(--sl-accent);color:var(--sl-accent-ink)}
1056
+ .sl-powered-mark svg{width:11px;height:11px;fill:currentColor}
1057
+
759
1058
  /* a11y filter chips (flow within the top-left region) */
760
1059
  .sl-chips{display:flex;gap:6px;flex-wrap:wrap}
761
1060
  .sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
@@ -794,8 +1093,30 @@ var CSS = `
794
1093
  .sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
795
1094
  transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
796
1095
 
1096
+ /* hover preview \u2014 a COMPACT echo of the confirm card (deliberately smaller: it's
1097
+ a passing preview on hover, not the click/select action surface). Reuses the
1098
+ Section\xB7Row\xB7Seat identity grid so hover, confirm and the cart chip all share
1099
+ one visual language, just at three sizes. */
1100
+ .sl-tip{position:absolute;z-index:7;pointer-events:none;display:none;width:190px;overflow:hidden;
1101
+ background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:11px;
1102
+ box-shadow:0 12px 30px -14px rgba(0,0,0,.6)}
1103
+ .sl-tip-grid{display:grid;grid-template-columns:1.3fr .85fr .85fr;border-bottom:1px solid var(--sl-line)}
1104
+ .sl-tip-grid.one{grid-template-columns:1fr}
1105
+ .sl-tip-field{min-width:0;padding:6px 9px;border-right:1px solid var(--sl-line)}
1106
+ .sl-tip-field:last-child{border-right:0;text-align:center}
1107
+ .sl-tip-grid:not(.one) .sl-tip-field:nth-child(2){text-align:center}
1108
+ .sl-tip-key{display:block;font-size:7.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
1109
+ .sl-tip-val{display:block;margin-top:2px;color:var(--sl-text);font-size:13px;line-height:1.1;font-weight:750;
1110
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1111
+ .sl-tip-cat{display:flex;align-items:center;gap:7px;padding:6px 10px;font-size:11px;
1112
+ background:color-mix(in srgb,var(--sl-cat) 12%,var(--sl-surface))}
1113
+ .sl-tip-dot{width:8px;height:8px;border-radius:50%;flex:none}
1114
+ .sl-tip-name{color:var(--sl-muted);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1115
+ .sl-tip-amt{margin-left:auto;font-weight:800;color:var(--sl-text);font-variant-numeric:tabular-nums;font-size:12px}
1116
+ .sl-tip-status{padding:5px 10px 7px;font-size:8.5px;letter-spacing:.09em;text-transform:uppercase;font-weight:700;color:var(--sl-muted)}
1117
+
797
1118
  /* Best available is a first-class shortcut, not an anonymous utility row. */
798
- .sl-ba{position:relative;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
1119
+ .sl-ba{position:relative;flex:none;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
799
1120
  padding:13px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));border-radius:13px;
800
1121
  background:linear-gradient(135deg,color-mix(in srgb,var(--sl-accent) 5%,var(--sl-surface)),color-mix(in srgb,var(--sl-accent) 11%,var(--sl-surface)))}
801
1122
  .sl-ba::after{content:'\u2726';position:absolute;right:10px;top:3px;color:color-mix(in srgb,var(--sl-accent) 20%,transparent);font-size:42px;line-height:1}
@@ -831,12 +1152,6 @@ var CSS = `
831
1152
  /* per-seat ticket-tier select + view-from-seat button in tray chips */
832
1153
  .sl-chip .tier{background:var(--sl-bg);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:6px;
833
1154
  font:inherit;font-size:10px;padding:2px 4px;min-width:0;max-width:100%;cursor:pointer}
834
- .sl-chip .view{width:20px;height:20px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
835
- color:var(--sl-muted);opacity:.36;transition:color .15s,opacity .15s}
836
- .sl-chip .view:hover{color:var(--sl-text)}
837
- .sl-chip:hover .view,.sl-chip .view:focus-visible{opacity:1;color:var(--sl-text)}
838
- .sl-chip .view svg{width:12px;height:12px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
839
- @media(pointer:coarse){.sl-chip .view{opacity:.58}}
840
1155
 
841
1156
  /* arena: LOD rung pills (flow within the top-center region) */
842
1157
  .sl-rungs{display:none;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
@@ -894,6 +1209,14 @@ var CSS = `
894
1209
  .sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
895
1210
 
896
1211
  /* view-from-seat button on the confirm popover */
1212
+ /* Eager sightline preview inside the confirm card */
1213
+ .sl-confirm-thumbwrap{position:relative;display:block;width:100%;height:74px;margin:0 0 8px;padding:0!important;
1214
+ border-radius:9px;overflow:hidden;border:1px solid var(--sl-line);cursor:pointer}
1215
+ .sl-confirm-thumb{display:block;width:100%;height:100%;object-fit:cover}
1216
+ .sl-confirm-thumb-badge{position:absolute;right:7px;top:7px;display:inline-flex;align-items:center;gap:5px;
1217
+ font-size:10px;font-weight:700;color:#fff;background:rgba(10,14,22,0.72);border-radius:12px;padding:4px 9px;backdrop-filter:blur(3px)}
1218
+ .sl-confirm-sight{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--sl-muted);margin-bottom:2px}
1219
+ .sl-confirm-sight span{color:#22a06b;font-weight:800}
897
1220
  .sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
898
1221
  color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
899
1222
  .sl-confirm-view:hover{border-color:var(--sl-muted)}
@@ -977,6 +1300,22 @@ function resolveTokens(chart, host) {
977
1300
  "--sl-radius": `${host?.radius ?? 14}px`
978
1301
  };
979
1302
  }
1303
+ var CB_STORAGE_KEY = "seatmap.a11y.cb";
1304
+ function readStoredColorblind() {
1305
+ try {
1306
+ if (typeof window === "undefined") return null;
1307
+ const raw = window.localStorage.getItem(CB_STORAGE_KEY);
1308
+ return raw == null ? null : raw === "1";
1309
+ } catch {
1310
+ return null;
1311
+ }
1312
+ }
1313
+ function writeStoredColorblind(on) {
1314
+ try {
1315
+ window.localStorage.setItem(CB_STORAGE_KEY, on ? "1" : "0");
1316
+ } catch {
1317
+ }
1318
+ }
980
1319
  var SeatPicker = class _SeatPicker {
981
1320
  constructor(options) {
982
1321
  this.root = null;
@@ -1009,11 +1348,17 @@ var SeatPicker = class _SeatPicker {
1009
1348
  this.confirmEl = null;
1010
1349
  this.confirmSeat = null;
1011
1350
  this.srEl = null;
1012
- this.a11yFilter = "all";
1013
1351
  this.baQty = 2;
1014
1352
  this.baCat = "";
1015
1353
  this.bestAvailableConfirm = false;
1016
1354
  this.releasingHold = false;
1355
+ /** Event sales window is closed (read-only load state / live close). */
1356
+ this.salesClosed = false;
1357
+ /** Every seated category's live availability is 0 (sold-out overlay is up). */
1358
+ this.soldOut = false;
1359
+ this.soldoutEl = null;
1360
+ /** Resolved colorblind-safe state — stored preference wins over the option. */
1361
+ this.cbSafe = false;
1017
1362
  // arena / multi-floor / seat-view chrome
1018
1363
  this.rungsEl = null;
1019
1364
  this.floorsEl = null;
@@ -1022,13 +1367,11 @@ var SeatPicker = class _SeatPicker {
1022
1367
  this.viewCleanup = null;
1023
1368
  this.allSeatsCache = null;
1024
1369
  // F3 minimap
1025
- this.miniEl = null;
1026
1370
  this.miniCanvas = null;
1027
1371
  this.miniBase = null;
1028
1372
  this.miniTf = null;
1029
1373
  // F4 price-band filter — active band's category keys (null = all prices)
1030
1374
  this.priceBandKeys = null;
1031
- this.priceFilterEl = null;
1032
1375
  /** Last surfaced section summary (re-rendered when the price band changes). */
1033
1376
  this.lastSection = null;
1034
1377
  /** Section card collapsed to its slim pill (seat-picking has begun). */
@@ -1048,6 +1391,9 @@ var SeatPicker = class _SeatPicker {
1048
1391
  this.ctaPhase = "idle";
1049
1392
  // narrow-layout chrome that docks into the sheet's Filters row on mobile
1050
1393
  this.a11yChipsEl = null;
1394
+ this.fsFallback = false;
1395
+ this.fsChangeHandler = null;
1396
+ this.fsEscHandler = null;
1051
1397
  this.cbEl = null;
1052
1398
  // modal plumbing (set by open())
1053
1399
  this.modalScrim = null;
@@ -1055,20 +1401,22 @@ var SeatPicker = class _SeatPicker {
1055
1401
  this.escHandler = null;
1056
1402
  /** Set by open(): closes the modal (scroll restore + destroy + onClose). */
1057
1403
  this.closeModal = null;
1404
+ this.lastCatAvail = null;
1058
1405
  if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
1059
1406
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
1060
1407
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
1061
1408
  this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
1062
1409
  this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
1063
- this.api = new PubApi(this.apiBase);
1410
+ this.api = options.transport ?? new PubApi(this.apiBase);
1064
1411
  this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
1412
+ this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
1065
1413
  this.controller = new import_core2.PickerController({
1066
1414
  transport: this.api,
1067
1415
  eventKey: options.event,
1068
1416
  maxSelection: this.maxTickets,
1069
1417
  currency: options.currency,
1070
1418
  flashOnLiveChange: true,
1071
- colorblindSafe: options.colorblindSafe,
1419
+ colorblindSafe: this.cbSafe,
1072
1420
  onSelectionChange: () => {
1073
1421
  this.syncTray();
1074
1422
  if (this.committedSelection().length) this.collapseSectionCard();
@@ -1094,6 +1442,11 @@ var SeatPicker = class _SeatPicker {
1094
1442
  },
1095
1443
  confirmSelection: this.opts.confirmSelection,
1096
1444
  onSelect: (seat) => {
1445
+ if (this.salesClosed) {
1446
+ this.controller.deselect([seat.id]);
1447
+ this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
1448
+ return;
1449
+ }
1097
1450
  this.flashPickedSeat(seat.id);
1098
1451
  if (this.opts.confirmSelection) this.showConfirm(seat);
1099
1452
  },
@@ -1116,9 +1469,68 @@ var SeatPicker = class _SeatPicker {
1116
1469
  onHint: (m) => {
1117
1470
  if (m) this.toast(m);
1118
1471
  },
1472
+ // Server declared the event closed mid-session (409 event_closed) — keep
1473
+ // the toast (raised by handleCta), and add the persistent read-only state.
1474
+ onSalesClosed: () => this.setSalesClosed(true),
1119
1475
  onError: (err) => this.opts.onError?.(err)
1120
1476
  });
1121
1477
  }
1478
+ /**
1479
+ * Eager sightline preview for the confirm card: a cheap generated forward
1480
+ * view (or the organizer's real photo) plus a "Nm to stage · clear
1481
+ * sightline" line — the premium at-a-glance moment; click opens the 360.
1482
+ */
1483
+ confirmThumbHtml(seat) {
1484
+ const doc = this.controller.doc;
1485
+ if (!doc) return "";
1486
+ let url = seat.viewUrl ?? "";
1487
+ let distance = null;
1488
+ if (!url) {
1489
+ try {
1490
+ const thumb = (0, import_core2.generateSeatThumb)(seat, doc.focalPoint);
1491
+ url = thumb.url;
1492
+ distance = thumb.distanceM ?? null;
1493
+ } catch {
1494
+ return "";
1495
+ }
1496
+ }
1497
+ const sight = distance != null ? `${distance}${this.tf("picker.sightline", "m to stage \xB7 clear sightline")}` : this.tf("picker.sightlineClear", "Clear sightline");
1498
+ return `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb" src="${url}" alt="" /><span class="sl-confirm-thumb-badge">\u{1F52D} ${this.tf("picker.viewFromHere", "View from here")}</span></button><div class="sl-confirm-sight"><span aria-hidden="true">\u2713</span>${sight}</div>`;
1499
+ }
1500
+ /** Full screen via the native API, falling back to a fixed-position overlay (iOS Safari). */
1501
+ toggleFullscreen() {
1502
+ const root = this.root;
1503
+ if (!root) return;
1504
+ const active = !!document.fullscreenElement || this.fsFallback;
1505
+ if (!active) {
1506
+ if (root.requestFullscreen) {
1507
+ root.requestFullscreen().catch(() => this.setFsFallback(true));
1508
+ } else {
1509
+ this.setFsFallback(true);
1510
+ }
1511
+ } else if (document.fullscreenElement) {
1512
+ void document.exitFullscreen().catch(() => {
1513
+ });
1514
+ } else {
1515
+ this.setFsFallback(false);
1516
+ }
1517
+ }
1518
+ setFsFallback(on) {
1519
+ if (this.fsFallback === on) return;
1520
+ this.fsFallback = on;
1521
+ this.root?.classList.toggle("sl-fs", on);
1522
+ this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
1523
+ if (on && !this.fsEscHandler) {
1524
+ this.fsEscHandler = (e) => {
1525
+ if (e.key === "Escape" && !document.fullscreenElement) this.setFsFallback(false);
1526
+ };
1527
+ window.addEventListener("keydown", this.fsEscHandler);
1528
+ } else if (!on && this.fsEscHandler) {
1529
+ window.removeEventListener("keydown", this.fsEscHandler);
1530
+ this.fsEscHandler = null;
1531
+ }
1532
+ requestAnimationFrame(() => this.controller.zoomToFit());
1533
+ }
1122
1534
  /**
1123
1535
  * Close the picker. In modal mode (SeatPicker.open()) this dismisses the
1124
1536
  * modal exactly like ESC/scrim/✕ — restores page scroll and fires onClose.
@@ -1213,6 +1625,10 @@ var SeatPicker = class _SeatPicker {
1213
1625
  <div class="sl-head-meta" data-ref="meta"></div>
1214
1626
  </div>
1215
1627
  <span class="sl-hold-pill" data-ref="hold"></span>
1628
+ <span class="sl-closed-pill" data-ref="closedPill" role="status">
1629
+ <svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
1630
+ <span data-ref="closedPillText"></span>
1631
+ </span>
1216
1632
  <button type="button" class="sl-close" data-ref="close" aria-label="Close">
1217
1633
  <svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
1218
1634
  </button>
@@ -1226,6 +1642,9 @@ var SeatPicker = class _SeatPicker {
1226
1642
  <button type="button" aria-label="Fit to screen" data-ref="zfit">
1227
1643
  <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>
1228
1644
  </button>
1645
+ <button type="button" aria-label="Full screen" aria-pressed="false" data-ref="zfs">
1646
+ <svg viewBox="0 0 24 24"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
1647
+ </button>
1229
1648
  </div>
1230
1649
  <div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
1231
1650
  <div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
@@ -1244,6 +1663,7 @@ var SeatPicker = class _SeatPicker {
1244
1663
  <div class="sl-filters" data-ref="filters"></div>
1245
1664
  <div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>Ticket prices</span></div>
1246
1665
  <div class="sl-prices" data-ref="prices"></div>
1666
+ <div class="sl-live" data-ref="live" role="status" aria-live="polite"><span class="dot" aria-hidden="true"></span><span data-ref="liveText">Live availability \u2014 seats update in real time</span></div>
1247
1667
  <div class="sl-sec sl-seats-sec"><span>Your seats</span><span class="sl-seat-summary" data-ref="seatSummary"></span></div>
1248
1668
  <div class="sl-tray" data-ref="tray"></div>
1249
1669
  <div class="sl-foot" data-ref="foot">
@@ -1277,6 +1697,13 @@ var SeatPicker = class _SeatPicker {
1277
1697
  this.els.zin.addEventListener("click", () => this.controller.zoomIn());
1278
1698
  this.els.zout.addEventListener("click", () => this.controller.zoomOut());
1279
1699
  this.els.zfit.addEventListener("click", () => this.controller.zoomToFit());
1700
+ this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
1701
+ this.fsChangeHandler = () => {
1702
+ if (!document.fullscreenElement) this.setFsFallback(false);
1703
+ this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback));
1704
+ requestAnimationFrame(() => this.controller.zoomToFit());
1705
+ };
1706
+ document.addEventListener("fullscreenchange", this.fsChangeHandler);
1280
1707
  const head = this.els.sheetHead;
1281
1708
  if (head) {
1282
1709
  const toggle = this.els.sheetToggle;
@@ -1320,7 +1747,7 @@ var SeatPicker = class _SeatPicker {
1320
1747
  }
1321
1748
  this.tipEl = document.createElement("div");
1322
1749
  this.tipEl.setAttribute("role", "tooltip");
1323
- this.tipEl.style.cssText = "position:absolute;z-index:7;pointer-events:none;display:none;max-width:240px;background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:10px;padding:9px 12px;font-size:12px;line-height:1.45;";
1750
+ this.tipEl.className = "sl-tip";
1324
1751
  this.els.map.appendChild(this.tipEl);
1325
1752
  this.els.map.addEventListener("mousemove", (e) => {
1326
1753
  const r = this.els.map.getBoundingClientRect();
@@ -1345,6 +1772,7 @@ var SeatPicker = class _SeatPicker {
1345
1772
  return this;
1346
1773
  }
1347
1774
  this.els.boot.remove();
1775
+ this.salesClosed = !!info.salesClosed;
1348
1776
  this.buildRegions();
1349
1777
  this.regions["bottom-right"].appendChild(this.els.zoom);
1350
1778
  this.regions["bottom-center"].appendChild(this.els.toast);
@@ -1364,6 +1792,7 @@ var SeatPicker = class _SeatPicker {
1364
1792
  this.els.name.textContent = info.eventName ?? "";
1365
1793
  const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
1366
1794
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
1795
+ this.buildBadge(chartTheme);
1367
1796
  const present = /* @__PURE__ */ new Set();
1368
1797
  if (this.controller.doc) {
1369
1798
  for (const seat of (0, import_core2.expandChart)(this.controller.doc)) {
@@ -1379,12 +1808,29 @@ var SeatPicker = class _SeatPicker {
1379
1808
  chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
1380
1809
  this.regions["top-left"].appendChild(chips);
1381
1810
  this.a11yChipsEl = chips;
1811
+ const active = /* @__PURE__ */ new Set();
1812
+ const syncChips = () => {
1813
+ chips.querySelectorAll("button").forEach((b) => {
1814
+ const f = b.dataset.f;
1815
+ const on = f === "all" ? active.size === 0 : active.has(f);
1816
+ b.classList.toggle("on", on);
1817
+ b.setAttribute("aria-pressed", String(on));
1818
+ });
1819
+ const filter = active.size ? [...active] : null;
1820
+ this.controller.setAccessibilityFilter(filter);
1821
+ if (filter && this.rungsEl && this.controller.getRung() !== "seats") {
1822
+ this.controller.setRung("seats");
1823
+ this.collapseSectionCard();
1824
+ this.syncRung();
1825
+ }
1826
+ };
1382
1827
  chips.querySelectorAll("button").forEach((btn) => {
1383
1828
  btn.addEventListener("click", () => {
1384
1829
  const f = btn.dataset.f;
1385
- this.a11yFilter = f;
1386
- chips.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
1387
- this.controller.setAccessibilityFilter(f === "all" ? null : [f]);
1830
+ if (f === "all") active.clear();
1831
+ else if (active.has(f)) active.delete(f);
1832
+ else active.add(f);
1833
+ syncChips();
1388
1834
  });
1389
1835
  });
1390
1836
  }
@@ -1393,14 +1839,14 @@ var SeatPicker = class _SeatPicker {
1393
1839
  cb.className = "sl-cbbtn";
1394
1840
  this.cbEl = cb;
1395
1841
  cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
1396
- cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
1842
+ cb.setAttribute("aria-pressed", String(this.cbSafe));
1397
1843
  cb.innerHTML = '<svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
1398
1844
  this.els.zfit.parentElement.appendChild(cb);
1399
- let cbOn = !!this.opts.colorblindSafe;
1400
1845
  cb.addEventListener("click", () => {
1401
- cbOn = !cbOn;
1402
- cb.setAttribute("aria-pressed", String(cbOn));
1403
- this.controller.setColorblindSafe(cbOn);
1846
+ this.cbSafe = !this.cbSafe;
1847
+ cb.setAttribute("aria-pressed", String(this.cbSafe));
1848
+ this.controller.setColorblindSafe(this.cbSafe);
1849
+ writeStoredColorblind(this.cbSafe);
1404
1850
  });
1405
1851
  this.srEl = document.createElement("div");
1406
1852
  this.srEl.className = "sl-sr";
@@ -1411,9 +1857,11 @@ var SeatPicker = class _SeatPicker {
1411
1857
  this.buildPriceFilter();
1412
1858
  this.buildExtendPrompt();
1413
1859
  this.buildBookedOverlay();
1860
+ this.buildSoldoutOverlay();
1414
1861
  this.dockLayoutChrome();
1415
1862
  await this.restoreRememberedHold();
1416
1863
  if (this.destroyed) return this;
1864
+ if (this.salesClosed) this.applySalesClosed();
1417
1865
  this.syncPrices();
1418
1866
  this.syncTray();
1419
1867
  return this;
@@ -1465,6 +1913,85 @@ var SeatPicker = class _SeatPicker {
1465
1913
  this.bookedEl = el;
1466
1914
  this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
1467
1915
  }
1916
+ /**
1917
+ * Localized string with a literal fallback. `t()` returns the key itself for
1918
+ * unknown keys, so this collapses that to `fallback` — while still honoring a
1919
+ * host `messages` override (which makes `t()` return the override, not the key).
1920
+ */
1921
+ tf(key, fallback) {
1922
+ const v = (0, import_core2.t)(key);
1923
+ return v === key ? fallback : v;
1924
+ }
1925
+ /** Sold-out overlay — centered over the map, disabled waitlist stub (Gap 2). */
1926
+ buildSoldoutOverlay() {
1927
+ if (!this.els.map) return;
1928
+ const el = document.createElement("div");
1929
+ el.className = "sl-soldout";
1930
+ el.setAttribute("role", "status");
1931
+ const name = (this.controller.doc?.theme?.brandName ?? this.opts.theme?.brandName ?? this.els.name?.textContent ?? this.tf("picker.soldOutEyebrow", "This event")).toUpperCase();
1932
+ 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>`;
1933
+ this.els.map.appendChild(el);
1934
+ this.soldoutEl = el;
1935
+ }
1936
+ /**
1937
+ * Recompute the sold-out state on every price/availability sync. Sold-out ⇔
1938
+ * every SEATED category's live free count is 0. Suppressed when the chart has
1939
+ * GA areas (GA capacity isn't per-seat, so seated counts would read 0 and
1940
+ * falsely block standing room) — mirrors the public page. Clears live when WS
1941
+ * frees a seat up.
1942
+ */
1943
+ syncSoldout(categories, left) {
1944
+ const hasGA = this.controller.getGAAreas().length > 0;
1945
+ const soldOut = this.isSoldOut(categories, left, hasGA);
1946
+ if (soldOut === this.soldOut) return;
1947
+ this.soldOut = soldOut;
1948
+ this.soldoutEl?.classList.toggle("on", soldOut);
1949
+ }
1950
+ /**
1951
+ * Pure sold-out predicate: every SEATED category's free count is 0, there is at
1952
+ * least one seated category, and there are no GA areas (GA capacity isn't
1953
+ * per-seat, so seated counts read 0 and would falsely block standing room).
1954
+ * `left` is seeded implicitly — a missing key means a fully-booked tier (0 free).
1955
+ */
1956
+ isSoldOut(categories, left, hasGA) {
1957
+ return !hasGA && categories.length > 0 && categories.every((c) => (left[c.key] ?? 0) === 0);
1958
+ }
1959
+ /**
1960
+ * Sales-closed read-only state (Gap 3): persistent header pill, disabled CTA
1961
+ * with a closed label, and frozen best-available / GA controls. `setSalesClosed`
1962
+ * is the reactive entry (live 409 event_closed); `applySalesClosed` is the
1963
+ * idempotent DOM apply used at load and on transition.
1964
+ */
1965
+ setSalesClosed(closed) {
1966
+ if (this.salesClosed === closed) return;
1967
+ this.salesClosed = closed;
1968
+ this.applySalesClosed();
1969
+ }
1970
+ applySalesClosed() {
1971
+ const pill = this.els.closedPill;
1972
+ if (pill) {
1973
+ pill.classList.toggle("on", this.salesClosed);
1974
+ const text = this.els.closedPillText ?? pill;
1975
+ text.textContent = this.tf("picker.salesClosedPill", "Sales are closed");
1976
+ }
1977
+ this.root?.setAttribute("data-sales-closed", String(this.salesClosed));
1978
+ this.syncCta();
1979
+ this.syncTray();
1980
+ }
1981
+ /** The badge is hidden when the host opts out OR the org's theme sets hideBadge. */
1982
+ badgeHidden(chartTheme) {
1983
+ return !!(this.opts.hideBadge || chartTheme?.hideBadge);
1984
+ }
1985
+ /** Attribution badge in the side-panel foot (Gap 7). Hidden per host/theme. */
1986
+ buildBadge(chartTheme) {
1987
+ if (this.badgeHidden(chartTheme)) return;
1988
+ const foot = this.els.foot;
1989
+ if (!foot) return;
1990
+ const el = document.createElement("div");
1991
+ el.className = "sl-powered";
1992
+ el.innerHTML = `<span class="sl-powered-mark" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 15c0-1.1.9-2 2-2h12a2 2 0 0 1 2 2v3h-3v-2H7v2H4v-3Z"/><rect x="7" y="7" width="10" height="5" rx="1.6"/></svg></span><span>${this.tf("picker.poweredBy", "Powered by SeatLayer")}</span>`;
1993
+ foot.appendChild(el);
1994
+ }
1468
1995
  // ---- Feature 6: chrome anchor regions -------------------------------------
1469
1996
  /**
1470
1997
  * Create the positioned flex containers that own every persistent map overlay.
@@ -1577,13 +2104,18 @@ var SeatPicker = class _SeatPicker {
1577
2104
  heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
1578
2105
  }
1579
2106
  return gaAreas.reduce(
1580
- (sum, area) => sum + area.price * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
2107
+ (sum, area) => sum + this.paidPrice(area.categoryKey, null, area.price) * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
1581
2108
  0
1582
2109
  );
1583
2110
  }
1584
2111
  syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
1585
2112
  const cta = this.els.cta;
1586
2113
  if (!cta) return;
2114
+ if (this.salesClosed) {
2115
+ cta.disabled = true;
2116
+ cta.textContent = this.tf("picker.salesClosedCta", "Sales closed");
2117
+ return;
2118
+ }
1587
2119
  if (this.confirmSeat) {
1588
2120
  cta.disabled = true;
1589
2121
  cta.textContent = "Confirm or cancel this seat";
@@ -1719,7 +2251,6 @@ var SeatPicker = class _SeatPicker {
1719
2251
  canvas.style.height = `${h}px`;
1720
2252
  wrap.appendChild(canvas);
1721
2253
  (this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
1722
- this.miniEl = wrap;
1723
2254
  this.miniCanvas = canvas;
1724
2255
  const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
1725
2256
  const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
@@ -1832,9 +2363,11 @@ var SeatPicker = class _SeatPicker {
1832
2363
  this.controller.overview();
1833
2364
  }
1834
2365
  // ---- F4 price-band filter -------------------------------------------------
1835
- /** Effective price of a category (first tier when tiered, else base price). */
2366
+ /** Effective display price of a category: host pricing override → first tier base. */
1836
2367
  catPrice(c) {
1837
- return c.tiers?.length ? c.tiers[0].price : c.price;
2368
+ const chart = c.tiers?.length ? c.tiers[0].price : c.price;
2369
+ if (chart === void 0 || !c.key) return chart;
2370
+ return this.paidPrice(c.key, c.tiers?.[0]?.id ?? null, chart);
1838
2371
  }
1839
2372
  /** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
1840
2373
  priceBands() {
@@ -1879,7 +2412,6 @@ var SeatPicker = class _SeatPicker {
1879
2412
  select.setAttribute("aria-label", "Filter and focus seats by price");
1880
2413
  select.innerHTML = `<option value="all">All prices</option>` + bands.map((band) => `<option value="${band.id}">${band.label}</option>`).join("");
1881
2414
  this.els.pricesSec.appendChild(select);
1882
- this.priceFilterEl = select;
1883
2415
  select.addEventListener("change", () => {
1884
2416
  const band = bands.find((candidate) => candidate.id === select.value);
1885
2417
  const keys = band?.keys ?? null;
@@ -1983,7 +2515,10 @@ var SeatPicker = class _SeatPicker {
1983
2515
  renderSectionCard(summary) {
1984
2516
  if (!this.els.map) return;
1985
2517
  this.secCardEl?.remove();
1986
- const priceLabel = summary.priceMin === summary.priceMax ? this.money(summary.priceMin) : `${this.money(summary.priceMin)}\u2013${this.money(summary.priceMax)}`;
2518
+ const paid = summary.categories.length ? summary.categories.map((c) => this.paidPrice(c.key, null, c.price)) : [summary.priceMin, summary.priceMax];
2519
+ const paidMin = Math.min(...paid);
2520
+ const paidMax = Math.max(...paid);
2521
+ const priceLabel = paidMin === paidMax ? this.money(paidMin) : `${this.money(paidMin)}\u2013${this.money(paidMax)}`;
1987
2522
  const leftLabel = (0, import_core2.tCount)("picker.seatsLeftInSection", summary.seatsLeft);
1988
2523
  const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${(0, import_core2.t)("picker.closeSectionSummary")}">\u2715</button>`;
1989
2524
  const card = document.createElement("div");
@@ -2013,7 +2548,7 @@ var SeatPicker = class _SeatPicker {
2013
2548
  card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
2014
2549
  const mix = summary.categories.map((c) => {
2015
2550
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2016
- return `<span class="sl-seccard-mix-item${dim ? " sl-dim" : ""}"><span class="sl-seccard-mix-dot" style="background:${c.color}"></span>${c.label} <span class="sl-seccard-mix-price">${this.money(c.price)}</span></span>`;
2551
+ return `<span class="sl-seccard-mix-item${dim ? " sl-dim" : ""}"><span class="sl-seccard-mix-dot" style="background:${c.color}"></span>${c.label} <span class="sl-seccard-mix-price">${this.money(this.paidPrice(c.key, null, c.price))}</span></span>`;
2017
2552
  }).join("");
2018
2553
  card.innerHTML = `<div class="sl-seccard-head"><span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn + `</div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${leftLabel}</span></div>` + (mix ? `<div class="sl-seccard-mix">${mix}</div>` : "") + `<div class="sl-seccard-foot"><button type="button" class="sl-seccard-overview">\u2190 ${(0, import_core2.t)("picker.overview")}</button><span class="sl-seccard-hint">${(0, import_core2.t)("picker.tapSeatHint")}</span></div>`;
2019
2554
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
@@ -2080,7 +2615,7 @@ var SeatPicker = class _SeatPicker {
2080
2615
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
2081
2616
  const status = this.controller.getStatus(seat.id) ?? "free";
2082
2617
  const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
2083
- const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
2618
+ const price = cat ? this.catPrice(cat) : void 0;
2084
2619
  this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
2085
2620
  }
2086
2621
  // ---- seat candidate confirmation ------------------------------------------
@@ -2095,7 +2630,8 @@ var SeatPicker = class _SeatPicker {
2095
2630
  if (this.tipEl) this.tipEl.style.display = "none";
2096
2631
  const details = this.controller.seatDetails(seat.id);
2097
2632
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
2098
- const price = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
2633
+ const chartPrice = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
2634
+ const price = chartPrice != null ? this.paidPrice(seat.categoryKey, details?.tierId ?? cat?.tiers?.[0]?.id ?? null, chartPrice) : void 0;
2099
2635
  const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
2100
2636
  "&": "&amp;",
2101
2637
  "<": "&lt;",
@@ -2108,7 +2644,7 @@ var SeatPicker = class _SeatPicker {
2108
2644
  el.setAttribute("aria-modal", "true");
2109
2645
  el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
2110
2646
  el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
2111
- el.innerHTML = `<div class="sl-confirm-grid"><div class="sl-confirm-field"><span class="sl-confirm-key">Section</span><span class="sl-confirm-value">${safe(details?.sectionLabel)}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Row</span><span class="sl-confirm-value">${safe(details?.rowLabel)}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Seat</span><span class="sl-confirm-value">${safe(details?.seatNumber ?? seat.label)}</span></div></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.seatViewEnabled() ? `<button type="button" class="sl-confirm-view"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>${(0, import_core2.t)("picker.open360")}</button>` : "") + `<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>`;
2647
+ el.innerHTML = `<div class="sl-confirm-grid"><div class="sl-confirm-field"><span class="sl-confirm-key">Section</span><span class="sl-confirm-value">${safe(details?.sectionLabel)}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Row</span><span class="sl-confirm-value">${safe(this.rowShort(details))}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Seat</span><span class="sl-confirm-value">${safe(details?.seatNumber ?? seat.label)}</span></div></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.seatViewEnabled() ? this.confirmThumbHtml(seat) : "") + `<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>`;
2112
2648
  this.els.map.appendChild(el);
2113
2649
  this.confirmEl = el;
2114
2650
  this.reanchorConfirm();
@@ -2273,18 +2809,35 @@ var SeatPicker = class _SeatPicker {
2273
2809
  }
2274
2810
  // ---- chrome sync ----------------------------------------------------------
2275
2811
  money(n) {
2812
+ const formatter = this.opts.pricing?.formatter;
2813
+ if (formatter) return formatter(n, this.currency);
2276
2814
  try {
2277
2815
  return new Intl.NumberFormat(this.opts.locale, { style: "currency", currency: this.currency }).format(n);
2278
2816
  } catch {
2279
2817
  return `${n} ${this.currency}`;
2280
2818
  }
2281
2819
  }
2820
+ /**
2821
+ * The price the buyer will actually pay for a category (+tier): the host's
2822
+ * `pricing` override when present, else the chart's stored price. Every
2823
+ * price the widget DISPLAYS or hands off must flow through here — a map
2824
+ * that shows one price while checkout charges another destroys trust.
2825
+ */
2826
+ paidPrice(categoryKey, tierId, fallback) {
2827
+ const entry = categoryKey ? this.opts.pricing?.prices?.[categoryKey] : void 0;
2828
+ if (entry === void 0) return fallback;
2829
+ if (typeof entry === "number") return entry;
2830
+ if (tierId && entry.tiers?.[tierId] !== void 0) return entry.tiers[tierId];
2831
+ return entry.base ?? fallback;
2832
+ }
2282
2833
  syncPrices() {
2283
2834
  const doc = this.controller.doc;
2284
2835
  if (!doc || !this.els.prices) return;
2285
2836
  const left = this.controller.categoryAvailability();
2837
+ this.narrateAvailability(doc.categories, left);
2838
+ this.syncSoldout(doc.categories, left);
2286
2839
  this.els.prices.innerHTML = doc.categories.map((c) => {
2287
- const price = c.tiers?.length ? c.tiers[0].price : c.price;
2840
+ const price = this.catPrice(c);
2288
2841
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2289
2842
  return `<div class="sl-price-row${dim ? " sl-dim" : ""}" data-cat="${c.key}"><span class="sl-dot" style="background:${c.color}"></span><span class="sl-price-label">${c.label}</span><span class="sl-price-left">${left[c.key] ?? 0} left</span>` + (price != null ? `<span class="sl-price-amt">${this.money(price)}</span>` : "") + `</div>`;
2290
2843
  }).join("") + `<div class="sl-status-key" aria-label="Seat status legend"><span class="sl-status-item"><i class="sl-status-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></i>Temporarily held</span><span class="sl-status-item"><i class="sl-status-icon sold" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 17L17 7"/></svg></i>Sold</span></div>`;
@@ -2293,6 +2846,26 @@ var SeatPicker = class _SeatPicker {
2293
2846
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
2294
2847
  });
2295
2848
  }
2849
+ /**
2850
+ * Live-activity strip: turn WS availability deltas into one quiet line of
2851
+ * social proof ("2 seats just taken in VIP · 118 left"). Diffs per-category
2852
+ * counts on every status change — no per-seat payload needed. Skips the very
2853
+ * first computation (initial load is not "activity").
2854
+ */
2855
+ narrateAvailability(categories, left) {
2856
+ const textEl = this.els.liveText;
2857
+ const prev = this.lastCatAvail;
2858
+ this.lastCatAvail = { ...left };
2859
+ if (!textEl || !prev) return;
2860
+ for (const cat of categories) {
2861
+ const before = prev[cat.key];
2862
+ const now = left[cat.key] ?? 0;
2863
+ if (before === void 0 || now >= before) continue;
2864
+ const taken = before - now;
2865
+ textEl.textContent = `${taken} seat${taken === 1 ? "" : "s"} just taken in ${cat.label} \xB7 ${now} left`;
2866
+ return;
2867
+ }
2868
+ }
2296
2869
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
2297
2870
  evictTakenSelections() {
2298
2871
  const ownLabels = /* @__PURE__ */ new Set([
@@ -2321,15 +2894,23 @@ var SeatPicker = class _SeatPicker {
2321
2894
  const cats = this.controller.doc?.categories ?? [];
2322
2895
  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>` + (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>`) + `<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>`);
2323
2896
  }
2897
+ const idGrid = (seatId, label) => {
2898
+ const d = seatId ? this.controller.seatDetails(seatId) : null;
2899
+ if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
2900
+ return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
2901
+ }
2902
+ return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Section</span><span class="val">${d.sectionLabel ?? "\u2014"}</span></span>` + (d.rowLabel ? `<span class="fld mid"><span class="sl-chip-eb">Row</span><span class="val">${this.rowShort(d)}</span></span>` : "") + (d.seatNumber ? `<span class="fld mid"><span class="sl-chip-eb">Seat</span><span class="val">${d.seatNumber}</span></span>` : "") + `</div>`;
2903
+ };
2904
+ const iconRail = (rmAria, viewLabel) => `<div class="sl-chip-rail"><button type="button" class="rm" aria-label="${rmAria}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>` + (viewLabel ? `<button type="button" class="view" data-view-label="${viewLabel}" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: viewLabel })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") + `</div>`;
2324
2905
  for (const item of heldItems) {
2325
2906
  const itemKey = `held:${item.label}`;
2326
2907
  nextTrayKeys.add(itemKey);
2327
2908
  const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
2328
2909
  const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
2329
- const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
2330
- const viewBtn = canView2 ? `<button type="button" class="view" data-view-label="${item.label}" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: item.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "";
2910
+ const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
2911
+ const canView2 = this.seatViewEnabled() && !!heldSeat;
2331
2912
  parts.push(
2332
- `<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-held="${encodeURIComponent(item.label)}"><span class="sl-ticket-state held" aria-label="Held for you" title="Held for you"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></span><div class="sl-chip-main"><b>${item.label}</b><div class="sl-chip-sub"><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span>${viewBtn}</div></div><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span><button type="button" class="rm" aria-label="Remove held ticket ${item.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
2913
+ `<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-held="${encodeURIComponent(item.label)}"${heldSeat ? ` data-locate="${heldSeat.id}"` : ""}><div class="sl-chip-main">` + idGrid(heldSeat?.id ?? null, item.label) + `<div class="sl-chip-sub"><span class="sl-ticket-state held" aria-label="Held for you" title="Held for you"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></span><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span><span class="amt">${this.money(this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1))}</span></div></div>` + iconRail(`Remove held ticket ${item.label}`, canView2 ? item.label : null) + `</div>`
2333
2914
  );
2334
2915
  }
2335
2916
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -2338,16 +2919,15 @@ var SeatPicker = class _SeatPicker {
2338
2919
  const itemKey = `seat:${s.id}`;
2339
2920
  nextTrayKeys.add(itemKey);
2340
2921
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
2341
- const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${(0, import_core2.t)("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(ti.price)}</option>`).join("") + `</select>` : "";
2342
- const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: s.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "";
2922
+ const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${(0, import_core2.t)("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(this.paidPrice(s.categoryKey, ti.id, ti.price))}</option>`).join("") + `</select>` : "";
2343
2923
  parts.push(
2344
- `<div class="sl-chip${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-seat="${s.id}"><span class="sl-ticket-state" aria-label="Selected" title="Selected"><svg viewBox="0 0 24 24"><path d="M5 12l4 4L19 6"/></svg></span><div class="sl-chip-main"><b>${s.label}</b><div class="sl-chip-sub"><span class="cat">${cat?.label ?? s.categoryKey}</span>${tierSelect}${viewBtn}</div></div><span class="amt">${this.money(s.price)}</span><button type="button" class="rm" aria-label="Remove ${s.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
2924
+ `<div class="sl-chip${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-seat="${s.id}" data-locate="${s.id}"><div class="sl-chip-main">` + idGrid(s.id, s.label) + `<div class="sl-chip-sub"><span class="sl-ticket-state" aria-label="Selected" title="Selected"><svg viewBox="0 0 24 24"><path d="M5 12l4 4L19 6"/></svg></span><span class="cat">${cat?.label ?? s.categoryKey}</span>${tierSelect}<span class="amt">${this.money(this.paidPrice(s.categoryKey, s.tierId ?? null, s.price))}</span></div></div>` + iconRail(`Remove ${s.label}`, canView ? s.label : null) + `</div>`
2345
2925
  );
2346
2926
  }
2347
2927
  for (const area of gaAreas) {
2348
2928
  const qty = this.gaQty.get(area.id) ?? 0;
2349
2929
  parts.push(
2350
- `<div class="sl-ga" data-ga="${area.id}"><div class="sl-ga-info"><div class="sl-ga-name">${area.label}</div><div class="sl-ga-sub">${this.money(area.price)} \xB7 ${area.available} left</div></div><div class="sl-ga-qty"><button type="button" data-d="-1" aria-label="Fewer">\u2212</button><span>${qty}</span><button type="button" data-d="1" aria-label="More">+</button></div></div>`
2930
+ `<div class="sl-ga" data-ga="${area.id}"><div class="sl-ga-info"><div class="sl-ga-name">${area.label}</div><div class="sl-ga-sub">${this.money(this.paidPrice(area.categoryKey, null, area.price))} \xB7 ${area.available} left</div></div><div class="sl-ga-qty"><button type="button" data-d="-1" aria-label="Fewer">\u2212</button><span>${qty}</span><button type="button" data-d="1" aria-label="More">+</button></div></div>`
2351
2931
  );
2352
2932
  }
2353
2933
  this.els.tray.innerHTML = parts.join("");
@@ -2387,7 +2967,7 @@ var SeatPicker = class _SeatPicker {
2387
2967
  return;
2388
2968
  }
2389
2969
  const id = chip.dataset.seat;
2390
- const label = chip.querySelector("b")?.textContent ?? "Seat";
2970
+ const label = this.controller.getSelection().find((sel) => sel.id === id)?.label ?? "Seat";
2391
2971
  const remove = () => {
2392
2972
  this.controller.deselect([id]);
2393
2973
  this.toast(`${label} removed.`, "neutral", {
@@ -2418,6 +2998,11 @@ var SeatPicker = class _SeatPicker {
2418
2998
  if (seat) this.openSeatView(seat);
2419
2999
  });
2420
3000
  });
3001
+ this.els.tray.querySelectorAll(".sl-chip[data-locate]").forEach((chip) => {
3002
+ const locate = () => this.controller.flashSeat(chip.dataset.locate, this.cssVar("--sl-accent") || "#f4b740");
3003
+ chip.addEventListener("mouseenter", locate);
3004
+ chip.addEventListener("focusin", locate);
3005
+ });
2421
3006
  this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
2422
3007
  btn.addEventListener("click", () => {
2423
3008
  const areaEl = btn.closest(".sl-ga");
@@ -2430,12 +3015,17 @@ var SeatPicker = class _SeatPicker {
2430
3015
  this.syncTray();
2431
3016
  });
2432
3017
  });
3018
+ if (this.salesClosed) {
3019
+ this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-replace],.sl-ga button").forEach((el) => {
3020
+ el.disabled = true;
3021
+ });
3022
+ }
2433
3023
  const gaTotal = this.pendingGATotal(gaAreas);
2434
3024
  const gaCount = this.pendingGACount();
2435
- const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
3025
+ const heldTotal = heldItems.reduce((sum, item) => sum + this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1), 0);
2436
3026
  const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
2437
3027
  const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
2438
- const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
3028
+ const total = freshSeats.reduce((sum, s) => sum + this.paidPrice(s.categoryKey, s.tierId ?? null, s.price), 0) + gaTotal + heldTotal;
2439
3029
  const count = freshSeats.length + gaCount + heldCount;
2440
3030
  const pendingCount = this.pendingSelectionCount();
2441
3031
  const previousCount = this.lastTrayCount;
@@ -2531,6 +3121,7 @@ var SeatPicker = class _SeatPicker {
2531
3121
  }
2532
3122
  }
2533
3123
  async handleCta() {
3124
+ if (this.salesClosed) return;
2534
3125
  if (this.totalTicketCount() > this.maxTickets) {
2535
3126
  this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
2536
3127
  return;
@@ -2574,6 +3165,7 @@ var SeatPicker = class _SeatPicker {
2574
3165
  this.opts.onError?.(err);
2575
3166
  const problem = err;
2576
3167
  const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
3168
+ if (problem.reason === "event_closed") this.setSalesClosed(true);
2577
3169
  const message = problem.reason === "event_closed" ? "Seat sales have closed for this event." : labels.length ? `${labels.join(", ")} ${labels.length === 1 ? "is" : "are"} no longer available. Choose another ${labels.length === 1 ? "seat" : "group"}.` : "One or more seats were just taken. Please pick again.";
2578
3170
  this.toast(message, "error");
2579
3171
  this.setCtaPhase("idle");
@@ -2682,7 +3274,7 @@ var SeatPicker = class _SeatPicker {
2682
3274
  objectType: it.objectType,
2683
3275
  categoryKey: it.categoryKey,
2684
3276
  tierId: it.tierId,
2685
- unitPrice: it.unitPrice,
3277
+ unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
2686
3278
  currency: it.currency ?? this.currency,
2687
3279
  quantity: it.quantity ?? 1
2688
3280
  }));
@@ -2735,19 +3327,37 @@ var SeatPicker = class _SeatPicker {
2735
3327
  this.tipEl.style.left = `${Math.max(8, x)}px`;
2736
3328
  this.tipEl.style.top = `${Math.max(8, y)}px`;
2737
3329
  }
3330
+ /**
3331
+ * Row label without the redundant section prefix. Charts commonly name row
3332
+ * objects "104-A" while the Section column already shows "104" — so the Row
3333
+ * cell repeats the section and, in the compact hover card, truncates to
3334
+ * "10…". Strip a leading "<section><sep>" so Row reads a clean "A". Only when
3335
+ * the prefix is exact (won't touch "1040-A" under section "104"); otherwise
3336
+ * the label is shown verbatim.
3337
+ */
3338
+ rowShort(details) {
3339
+ const row = details?.rowLabel;
3340
+ const sec = details?.sectionLabel;
3341
+ if (!row || !sec) return row;
3342
+ for (const sep of ["-", " ", "\xB7", "/", "_"]) {
3343
+ const prefix = `${sec}${sep}`;
3344
+ if (row.startsWith(prefix) && row.length > prefix.length) return row.slice(prefix.length);
3345
+ }
3346
+ return row;
3347
+ }
2738
3348
  updateTooltip(details) {
2739
3349
  if (!this.tipEl) return;
2740
3350
  if (!details) {
2741
3351
  this.tipEl.style.display = "none";
2742
3352
  return;
2743
3353
  }
2744
- const statusLine = details.status === "free" ? "" : `<div style="margin-top:5px;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;font-weight:700">${details.status === "held" ? (0, import_core2.t)("map.statusHeld") : (0, import_core2.t)("map.statusTaken")}</div>`;
2745
- const location = [
2746
- details.sectionLabel ? `Section ${details.sectionLabel}` : "",
2747
- details.rowLabel ? `Row ${details.rowLabel}` : "",
2748
- details.seatNumber ? `Seat ${details.seatNumber}` : details.label
2749
- ].filter(Boolean).join(" \xB7 ");
2750
- this.tipEl.innerHTML = `<div style="font-weight:800;font-size:13px">${location}</div><div style="display:flex;align-items:center;gap:6px;margin-top:4px"><span style="width:9px;height:9px;border-radius:50%;flex:none;background:${details.categoryColor}"></span><span style="opacity:.75">${details.categoryLabel}</span><span style="margin-left:auto;font-weight:800">${this.money(details.price)}</span></div>` + statusLine;
3354
+ const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
3355
+ const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
3356
+ const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
3357
+ const grid = hasLoc ? `<div class="sl-tip-grid"><div class="sl-tip-field"><span class="sl-tip-key">Section</span><span class="sl-tip-val">${esc2(details.sectionLabel)}</span></div><div class="sl-tip-field"><span class="sl-tip-key">Row</span><span class="sl-tip-val">${esc2(this.rowShort(details))}</span></div><div class="sl-tip-field"><span class="sl-tip-key">Seat</span><span class="sl-tip-val">${esc2(details.seatNumber ?? details.label)}</span></div></div>` : `<div class="sl-tip-grid one"><div class="sl-tip-field"><span class="sl-tip-key">Seat</span><span class="sl-tip-val">${esc2(details.label)}</span></div></div>`;
3358
+ const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? (0, import_core2.t)("map.statusHeld") : (0, import_core2.t)("map.statusTaken")}</div>`;
3359
+ this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
3360
+ this.tipEl.innerHTML = grid + `<div class="sl-tip-cat"><span class="sl-tip-dot" style="background:${details.categoryColor}"></span><span class="sl-tip-name">${esc2(details.categoryLabel)}</span><span class="sl-tip-amt">${price}</span></div>` + statusLine;
2751
3361
  this.tipEl.style.display = "block";
2752
3362
  this.placeTooltip();
2753
3363
  }
@@ -2768,7 +3378,7 @@ var SeatPicker = class _SeatPicker {
2768
3378
  return this.removeHeldLabel(label);
2769
3379
  }
2770
3380
  async bestAvailable(qty, categoryKey) {
2771
- if (this.bestAvailableBusy) return null;
3381
+ if (this.salesClosed || this.bestAvailableBusy) return null;
2772
3382
  qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
2773
3383
  if (this.confirmSeat) this.cancelConfirm();
2774
3384
  this.bestAvailableConfirm = false;
@@ -2849,6 +3459,8 @@ var SeatPicker = class _SeatPicker {
2849
3459
  this.ro?.disconnect();
2850
3460
  this.ro = null;
2851
3461
  if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
3462
+ if (this.fsChangeHandler) document.removeEventListener("fullscreenchange", this.fsChangeHandler);
3463
+ if (this.fsEscHandler) window.removeEventListener("keydown", this.fsEscHandler);
2852
3464
  this.controller.destroy();
2853
3465
  this.root?.remove();
2854
3466
  this.root = null;