@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.js CHANGED
@@ -326,16 +326,46 @@ var TYPES = /* @__PURE__ */ new Set([
326
326
  "seatlayer.designer.close",
327
327
  "seatlayer.designer.error"
328
328
  ]);
329
+ var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
329
330
  function resolveContainer2(container) {
330
331
  if (typeof container !== "string") return container;
331
332
  const element = document.querySelector(container);
332
333
  if (!element) throw new Error(`EmbeddedDesigner container not found: ${container}`);
333
334
  return element;
334
335
  }
336
+ function causeFromCode(code) {
337
+ const value = (code ?? "").toLowerCase();
338
+ if (value.includes("expire") || value.includes("revoke") || value === "401") return "expired";
339
+ if (value.includes("mismatch")) return "mismatch";
340
+ if (value.includes("timeout")) return "timeout";
341
+ return "load";
342
+ }
343
+ var ERROR_COPY = {
344
+ expired: {
345
+ title: "This design session expired",
346
+ body: "For your security, editing sessions are short-lived. Start a fresh one to keep designing."
347
+ },
348
+ mismatch: {
349
+ title: "This editor doesn't match this chart",
350
+ body: "The session that loaded belongs to a different chart or workspace. Reopen the designer to continue."
351
+ },
352
+ timeout: {
353
+ title: "The designer is taking too long",
354
+ body: "It did not finish loading in time. This is usually a slow connection \u2014 try again."
355
+ },
356
+ load: {
357
+ title: "We couldn't load the designer",
358
+ body: "Something went wrong while opening the editor. Please try again."
359
+ }
360
+ };
335
361
  var EmbeddedDesigner = class {
336
362
  constructor(options) {
337
363
  this.frame = null;
338
364
  this.designerOrigin = "";
365
+ this.overlay = null;
366
+ this.timeoutTimer = null;
367
+ this.phase = "loading";
368
+ this.restoreContainerPosition = null;
339
369
  this.handleMessage = (event) => {
340
370
  if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
341
371
  if (!event.data || typeof event.data !== "object") return;
@@ -350,10 +380,15 @@ var EmbeddedDesigner = class {
350
380
  message: typeof data.message === "string" ? data.message : void 0,
351
381
  meta: data.meta
352
382
  };
353
- if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId) return;
354
- if (this.options.expectedWorkspaceId && message.workspaceId && message.workspaceId !== this.options.expectedWorkspaceId) return;
383
+ if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId || this.options.expectedWorkspaceId && message.workspaceId && message.workspaceId !== this.options.expectedWorkspaceId) {
384
+ this.showError("mismatch");
385
+ return;
386
+ }
355
387
  switch (message.type) {
356
388
  case "seatlayer.designer.ready":
389
+ this.phase = "ready";
390
+ this.clearTimeoutTimer();
391
+ this.removeOverlay();
357
392
  this.options.onReady?.(message);
358
393
  break;
359
394
  case "seatlayer.designer.saved":
@@ -366,6 +401,7 @@ var EmbeddedDesigner = class {
366
401
  this.options.onClose?.(message);
367
402
  break;
368
403
  case "seatlayer.designer.error":
404
+ this.showError(causeFromCode(message.code));
369
405
  this.options.onError?.(message);
370
406
  break;
371
407
  }
@@ -389,9 +425,21 @@ var EmbeddedDesigner = class {
389
425
  frame.style.border = "0";
390
426
  Object.assign(frame.style, this.options.style);
391
427
  if (this.options.className) frame.className = this.options.className;
428
+ const container = resolveContainer2(this.options.container);
392
429
  window.addEventListener("message", this.handleMessage);
393
- resolveContainer2(this.options.container).append(frame);
430
+ container.append(frame);
394
431
  this.frame = frame;
432
+ this.phase = "loading";
433
+ if (this.loadingStateEnabled()) {
434
+ this.ensureContainerPositioned(container);
435
+ this.renderOverlay(container, "loading");
436
+ const timeout = this.options.loadingTimeoutMs ?? DEFAULT_LOADING_TIMEOUT_MS;
437
+ if (timeout > 0 && Number.isFinite(timeout)) {
438
+ this.timeoutTimer = setTimeout(() => {
439
+ if (this.phase === "loading") this.showError("timeout");
440
+ }, timeout);
441
+ }
442
+ }
395
443
  return frame;
396
444
  }
397
445
  /** Replace the iframe instead of assigning a new fragment to an existing one. */
@@ -404,9 +452,208 @@ var EmbeddedDesigner = class {
404
452
  }
405
453
  destroy() {
406
454
  window.removeEventListener("message", this.handleMessage);
455
+ this.clearTimeoutTimer();
456
+ this.removeOverlay();
457
+ this.restoreContainerStyle();
407
458
  this.frame?.remove();
408
459
  this.frame = null;
409
460
  this.designerOrigin = "";
461
+ this.phase = "loading";
462
+ }
463
+ loadingStateEnabled() {
464
+ return this.options.showLoadingState !== false;
465
+ }
466
+ clearTimeoutTimer() {
467
+ if (this.timeoutTimer !== null) {
468
+ clearTimeout(this.timeoutTimer);
469
+ this.timeoutTimer = null;
470
+ }
471
+ }
472
+ ensureContainerPositioned(container) {
473
+ const position = getComputedStyle(container).position;
474
+ if (position === "static") {
475
+ this.restoreContainerPosition = container.style.position;
476
+ container.style.position = "relative";
477
+ }
478
+ }
479
+ restoreContainerStyle() {
480
+ if (this.restoreContainerPosition === null) return;
481
+ try {
482
+ resolveContainer2(this.options.container).style.position = this.restoreContainerPosition;
483
+ } catch {
484
+ }
485
+ this.restoreContainerPosition = null;
486
+ }
487
+ removeOverlay() {
488
+ this.overlay?.remove();
489
+ this.overlay = null;
490
+ }
491
+ showError(cause) {
492
+ this.phase = "error";
493
+ this.clearTimeoutTimer();
494
+ if (!this.loadingStateEnabled()) return;
495
+ let container;
496
+ try {
497
+ container = resolveContainer2(this.options.container);
498
+ } catch {
499
+ return;
500
+ }
501
+ this.renderOverlay(container, "error", cause);
502
+ }
503
+ handleTryAgain() {
504
+ if (this.options.onRequestRelaunch) {
505
+ this.options.onRequestRelaunch();
506
+ return;
507
+ }
508
+ this.mount();
509
+ }
510
+ /**
511
+ * Build (or rebuild) the overlay for the given phase. A single overlay element
512
+ * is reused so we never stack stale skeletons or cards.
513
+ */
514
+ renderOverlay(container, phase, cause) {
515
+ this.removeOverlay();
516
+ const overlay = document.createElement("div");
517
+ overlay.setAttribute("data-seatlayer-designer-overlay", phase);
518
+ overlay.setAttribute("role", phase === "error" ? "alert" : "status");
519
+ overlay.setAttribute("aria-live", "polite");
520
+ Object.assign(overlay.style, {
521
+ position: "absolute",
522
+ inset: "0",
523
+ display: "flex",
524
+ alignItems: "center",
525
+ justifyContent: "center",
526
+ background: "#101625",
527
+ color: "#e6ebf5",
528
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
529
+ zIndex: "2",
530
+ overflow: "hidden"
531
+ });
532
+ if (phase === "loading") this.buildSkeleton(overlay);
533
+ else this.buildErrorCard(overlay, cause ?? "load");
534
+ container.append(overlay);
535
+ this.overlay = overlay;
536
+ }
537
+ buildSkeleton(overlay) {
538
+ const style = document.createElement("style");
539
+ style.textContent = `
540
+ @media (prefers-reduced-motion: no-preference) {
541
+ @keyframes seatlayer-designer-shimmer {
542
+ 0% { background-position: -320px 0; }
543
+ 100% { background-position: 320px 0; }
544
+ }
545
+ [data-seatlayer-designer-overlay="loading"] .sl-shimmer {
546
+ animation: seatlayer-designer-shimmer 1.25s ease-in-out infinite;
547
+ background-size: 640px 100%;
548
+ }
549
+ }`;
550
+ overlay.append(style);
551
+ 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%)";
552
+ const scaffold = document.createElement("div");
553
+ Object.assign(scaffold.style, {
554
+ position: "absolute",
555
+ inset: "0",
556
+ display: "flex",
557
+ flexDirection: "column",
558
+ padding: "16px",
559
+ gap: "14px",
560
+ opacity: "0.9"
561
+ });
562
+ const bar = (styles) => {
563
+ const node = document.createElement("div");
564
+ node.className = "sl-shimmer";
565
+ Object.assign(node.style, {
566
+ background: shimmer,
567
+ borderRadius: "8px"
568
+ });
569
+ Object.assign(node.style, styles);
570
+ return node;
571
+ };
572
+ scaffold.append(bar({ height: "40px", width: "100%", flex: "0 0 auto" }));
573
+ const body = document.createElement("div");
574
+ Object.assign(body.style, {
575
+ display: "flex",
576
+ gap: "14px",
577
+ flex: "1 1 auto",
578
+ minHeight: "0"
579
+ });
580
+ body.append(bar({ width: "220px", height: "100%", flex: "0 0 auto" }));
581
+ body.append(bar({ flex: "1 1 auto", height: "100%" }));
582
+ scaffold.append(body);
583
+ overlay.append(scaffold);
584
+ const caption = document.createElement("div");
585
+ Object.assign(caption.style, {
586
+ position: "relative",
587
+ zIndex: "1",
588
+ display: "flex",
589
+ alignItems: "center",
590
+ gap: "10px",
591
+ padding: "10px 16px",
592
+ borderRadius: "999px",
593
+ background: "rgba(16, 22, 37, 0.72)",
594
+ fontSize: "13px",
595
+ fontWeight: "500",
596
+ letterSpacing: "0.01em"
597
+ });
598
+ const dot = document.createElement("span");
599
+ dot.className = "sl-shimmer";
600
+ Object.assign(dot.style, {
601
+ width: "9px",
602
+ height: "9px",
603
+ borderRadius: "50%",
604
+ background: shimmer,
605
+ flex: "0 0 auto"
606
+ });
607
+ caption.append(dot);
608
+ caption.append(document.createTextNode("Loading designer\u2026"));
609
+ overlay.append(caption);
610
+ }
611
+ buildErrorCard(overlay, cause) {
612
+ const copy = ERROR_COPY[cause];
613
+ const card = document.createElement("div");
614
+ Object.assign(card.style, {
615
+ maxWidth: "420px",
616
+ margin: "0 24px",
617
+ padding: "28px",
618
+ textAlign: "center",
619
+ background: "rgba(255, 255, 255, 0.03)",
620
+ border: "1px solid rgba(255, 255, 255, 0.08)",
621
+ borderRadius: "16px",
622
+ boxShadow: "0 12px 40px rgba(0, 0, 0, 0.35)"
623
+ });
624
+ const heading = document.createElement("h2");
625
+ heading.textContent = copy.title;
626
+ Object.assign(heading.style, {
627
+ margin: "0 0 8px",
628
+ fontSize: "17px",
629
+ fontWeight: "600",
630
+ color: "#f4f7ff"
631
+ });
632
+ const body = document.createElement("p");
633
+ body.textContent = copy.body;
634
+ Object.assign(body.style, {
635
+ margin: "0 0 20px",
636
+ fontSize: "13.5px",
637
+ lineHeight: "1.5",
638
+ color: "#aab4c8"
639
+ });
640
+ const button = document.createElement("button");
641
+ button.type = "button";
642
+ button.textContent = "Try again";
643
+ Object.assign(button.style, {
644
+ appearance: "none",
645
+ cursor: "pointer",
646
+ border: "0",
647
+ borderRadius: "10px",
648
+ padding: "10px 22px",
649
+ fontSize: "14px",
650
+ fontWeight: "600",
651
+ color: "#101625",
652
+ background: "#7aa2ff"
653
+ });
654
+ button.addEventListener("click", () => this.handleTryAgain());
655
+ card.append(heading, body, button);
656
+ overlay.append(card);
410
657
  }
411
658
  };
412
659
 
@@ -415,6 +662,7 @@ import {
415
662
  PickerController as PickerController2,
416
663
  expandChart,
417
664
  generateSeatPanorama,
665
+ generateSeatThumb,
418
666
  loadLocale as loadLocale2,
419
667
  setStringOverrides as setStringOverrides2,
420
668
  t as t2,
@@ -579,26 +827,43 @@ var CSS = `
579
827
  .sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
580
828
  overscroll-behavior:contain;scrollbar-gutter:stable}
581
829
  .sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
582
- .sl-chip{position:relative;display:grid;grid-template-columns:24px minmax(0,1fr) auto 30px;align-items:center;gap:8px;
583
- min-height:53px;padding:7px 7px 7px 9px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);
830
+ .sl-chip{position:relative;display:grid;grid-template-columns:minmax(0,1fr) 34px;align-items:stretch;
831
+ flex:none;min-height:53px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);overflow:hidden;
584
832
  background:var(--sl-surface);font-size:13px;transform-origin:center;transition:border-color .15s,background .15s}
585
833
  .sl-chip:hover{border-color:color-mix(in srgb,var(--sl-accent) 38%,var(--sl-line))}
586
834
  .sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
587
835
  .sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
588
836
  .sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
589
837
  box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
590
- .sl-ticket-state{width:23px;height:23px;border-radius:999px;display:flex;align-items:center;justify-content:center;
838
+ .sl-ticket-state{width:17px;height:17px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
591
839
  background:var(--sl-accent);color:var(--sl-accent-ink)}
592
840
  .sl-ticket-state.held{background:color-mix(in srgb,var(--sl-accent) 18%,var(--sl-surface));color:var(--sl-accent)}
593
- .sl-ticket-state svg{width:13px;height:13px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
594
- .sl-chip-main{min-width:0}
595
- .sl-chip b{display:block;font-weight:800;min-width:max-content;white-space:nowrap}
596
- .sl-chip-sub{display:flex;align-items:center;gap:5px;min-width:0;margin-top:2px}
841
+ .sl-ticket-state svg{width:10px;height:10px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
842
+ .sl-chip-main{min-width:0;padding:8px 10px 8px 11px;display:flex;flex-direction:column;justify-content:center;gap:5px}
843
+ .sl-chip-id{display:flex;gap:12px;min-width:0}
844
+ .sl-chip-id .fld{min-width:0}
845
+ .sl-chip-id .fld.sec{flex:1}
846
+ .sl-chip-id .fld.mid{flex:none;text-align:center}
847
+ .sl-chip-eb{display:block;font-size:8px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);margin-bottom:1px}
848
+ .sl-chip-id .val{display:block;font-weight:600;font-size:13px;line-height:1.25;white-space:nowrap}
849
+ .sl-chip-id .fld.sec .val{overflow:hidden;text-overflow:ellipsis}
850
+ .sl-chip-sub{display:flex;align-items:center;gap:6px;min-width:0}
597
851
  .sl-chip .cat{color:var(--sl-muted);font-size:10.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
598
852
  .sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums;flex:none;white-space:nowrap}
599
- .sl-chip .rm{width:29px;height:29px;border-radius:8px;flex:none;display:flex;align-items:center;justify-content:center;color:var(--sl-muted)}
600
- .sl-chip .rm:hover,.sl-chip .rm:focus-visible{color:var(--sl-accent);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
853
+ .sl-chip-rail{display:flex;flex-direction:column;border-left:1px solid var(--sl-line)}
854
+ .sl-chip .rm,.sl-chip .view{flex:1;min-height:26px;border-radius:0;display:flex;align-items:center;justify-content:center;
855
+ color:var(--sl-muted);transition:color .15s,background .15s}
856
+ .sl-chip .view{border-top:1px solid var(--sl-line)}
857
+ .sl-chip .rm:hover,.sl-chip .rm:focus-visible{color:#e5484d;background:color-mix(in srgb,#e5484d 9%,transparent)}
858
+ .sl-chip .view:hover,.sl-chip .view:focus-visible{color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
601
859
  .sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
860
+ .sl-chip .view svg{width:13px;height:13px;stroke:currentColor;stroke-width:1.8;fill:none}
861
+ /* live-activity strip \u2014 narrates WS availability deltas (social proof + urgency) */
862
+ .sl-live{display:flex;align-items:center;gap:7px;margin:10px 14px 0;padding:7px 9px;flex:none;
863
+ border:1px solid var(--sl-line);border-radius:8px;background:color-mix(in srgb,var(--sl-accent) 4%,var(--sl-surface));
864
+ font-size:11px;color:var(--sl-muted)}
865
+ .sl-live .dot{width:6px;height:6px;border-radius:999px;background:#22a06b;box-shadow:0 0 6px rgba(34,160,107,.75);flex:none}
866
+ .sl-live span:last-child{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
602
867
 
603
868
  /* GA rows */
604
869
  .sl-ga{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px dashed var(--sl-line);border-radius:var(--sl-r-sm)}
@@ -674,6 +939,8 @@ var CSS = `
674
939
 
675
940
  /* zoom column (flows within the bottom-right region) */
676
941
  .sl-zoom{display:flex;flex-direction:column;gap:6px}
942
+ /* CSS-fallback full screen (iOS Safari has no element fullscreen API) */
943
+ .sl-picker.sl-fs{position:fixed;inset:0;z-index:2147483000;width:auto;height:auto;max-height:none;border-radius:0}
677
944
  .sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
678
945
  color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
679
946
  .sl-zoom button:hover{border-color:var(--sl-muted)}
@@ -732,6 +999,39 @@ var CSS = `
732
999
  .sl-booked.on .sl-booked-title{animation-delay:.22s}
733
1000
  .sl-booked.on .sl-booked-sub{animation-delay:.3s}
734
1001
 
1002
+ /* sold-out overlay \u2014 every SEATED category's live availability is 0. Centered
1003
+ over the map; a stub (disabled) "Join waitlist" button, exactly like the page.
1004
+ Suppressed when GA areas exist (GA capacity isn't seat-counted). Clears live
1005
+ the moment WS frees a seat up. */
1006
+ .sl-soldout{position:absolute;inset:0;z-index:10;display:none;flex-direction:column;align-items:center;
1007
+ justify-content:center;text-align:center;gap:8px;padding:24px;
1008
+ background:color-mix(in srgb,var(--sl-bg) 82%,transparent);backdrop-filter:blur(4px)}
1009
+ .sl-soldout.on{display:flex}
1010
+ .sl-soldout-eyebrow{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--sl-accent);font-weight:800}
1011
+ .sl-soldout-title{font-size:32px;font-weight:800;color:var(--sl-text);line-height:1.05}
1012
+ .sl-soldout-copy{max-width:360px;font-size:13px;color:var(--sl-muted);line-height:1.5}
1013
+ .sl-picker .sl-soldout-btn{margin-top:10px;min-height:40px;padding:10px 18px;border-radius:var(--sl-r-sm);
1014
+ background:var(--sl-surface);color:var(--sl-muted);border:1px solid var(--sl-line);font-weight:800;font-size:13px;
1015
+ cursor:not-allowed;opacity:.85}
1016
+
1017
+ /* sales-closed pill (header) \u2014 persistent read-only state when the event's sales
1018
+ window is closed at load or closes live mid-session. Neutral (not accent) so it
1019
+ reads as "unavailable", distinct from the accent hold pill next to it. */
1020
+ .sl-closed-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
1021
+ background:color-mix(in srgb,var(--sl-text) 12%,var(--sl-surface));color:var(--sl-text);
1022
+ font-weight:700;font-size:12px;white-space:nowrap}
1023
+ .sl-closed-pill.on{display:inline-flex}
1024
+ .sl-closed-pill svg{width:13px;height:13px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
1025
+
1026
+ /* "Powered by SeatLayer" attribution badge (side-panel foot) \u2014 the small gold
1027
+ rounded logo mark + wordmark. Hidden when the host opts out or the org's paid
1028
+ theme sets hideBadge. */
1029
+ .sl-powered{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:10px;
1030
+ font-size:11px;letter-spacing:.03em;color:var(--sl-muted)}
1031
+ .sl-powered-mark{width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;
1032
+ background:var(--sl-accent);color:var(--sl-accent-ink)}
1033
+ .sl-powered-mark svg{width:11px;height:11px;fill:currentColor}
1034
+
735
1035
  /* a11y filter chips (flow within the top-left region) */
736
1036
  .sl-chips{display:flex;gap:6px;flex-wrap:wrap}
737
1037
  .sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
@@ -770,8 +1070,30 @@ var CSS = `
770
1070
  .sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
771
1071
  transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
772
1072
 
1073
+ /* hover preview \u2014 a COMPACT echo of the confirm card (deliberately smaller: it's
1074
+ a passing preview on hover, not the click/select action surface). Reuses the
1075
+ Section\xB7Row\xB7Seat identity grid so hover, confirm and the cart chip all share
1076
+ one visual language, just at three sizes. */
1077
+ .sl-tip{position:absolute;z-index:7;pointer-events:none;display:none;width:190px;overflow:hidden;
1078
+ background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:11px;
1079
+ box-shadow:0 12px 30px -14px rgba(0,0,0,.6)}
1080
+ .sl-tip-grid{display:grid;grid-template-columns:1.3fr .85fr .85fr;border-bottom:1px solid var(--sl-line)}
1081
+ .sl-tip-grid.one{grid-template-columns:1fr}
1082
+ .sl-tip-field{min-width:0;padding:6px 9px;border-right:1px solid var(--sl-line)}
1083
+ .sl-tip-field:last-child{border-right:0;text-align:center}
1084
+ .sl-tip-grid:not(.one) .sl-tip-field:nth-child(2){text-align:center}
1085
+ .sl-tip-key{display:block;font-size:7.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
1086
+ .sl-tip-val{display:block;margin-top:2px;color:var(--sl-text);font-size:13px;line-height:1.1;font-weight:750;
1087
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1088
+ .sl-tip-cat{display:flex;align-items:center;gap:7px;padding:6px 10px;font-size:11px;
1089
+ background:color-mix(in srgb,var(--sl-cat) 12%,var(--sl-surface))}
1090
+ .sl-tip-dot{width:8px;height:8px;border-radius:50%;flex:none}
1091
+ .sl-tip-name{color:var(--sl-muted);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1092
+ .sl-tip-amt{margin-left:auto;font-weight:800;color:var(--sl-text);font-variant-numeric:tabular-nums;font-size:12px}
1093
+ .sl-tip-status{padding:5px 10px 7px;font-size:8.5px;letter-spacing:.09em;text-transform:uppercase;font-weight:700;color:var(--sl-muted)}
1094
+
773
1095
  /* Best available is a first-class shortcut, not an anonymous utility row. */
774
- .sl-ba{position:relative;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
1096
+ .sl-ba{position:relative;flex:none;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
775
1097
  padding:13px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));border-radius:13px;
776
1098
  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)))}
777
1099
  .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}
@@ -807,12 +1129,6 @@ var CSS = `
807
1129
  /* per-seat ticket-tier select + view-from-seat button in tray chips */
808
1130
  .sl-chip .tier{background:var(--sl-bg);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:6px;
809
1131
  font:inherit;font-size:10px;padding:2px 4px;min-width:0;max-width:100%;cursor:pointer}
810
- .sl-chip .view{width:20px;height:20px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
811
- color:var(--sl-muted);opacity:.36;transition:color .15s,opacity .15s}
812
- .sl-chip .view:hover{color:var(--sl-text)}
813
- .sl-chip:hover .view,.sl-chip .view:focus-visible{opacity:1;color:var(--sl-text)}
814
- .sl-chip .view svg{width:12px;height:12px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
815
- @media(pointer:coarse){.sl-chip .view{opacity:.58}}
816
1132
 
817
1133
  /* arena: LOD rung pills (flow within the top-center region) */
818
1134
  .sl-rungs{display:none;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
@@ -870,6 +1186,14 @@ var CSS = `
870
1186
  .sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
871
1187
 
872
1188
  /* view-from-seat button on the confirm popover */
1189
+ /* Eager sightline preview inside the confirm card */
1190
+ .sl-confirm-thumbwrap{position:relative;display:block;width:100%;height:74px;margin:0 0 8px;padding:0!important;
1191
+ border-radius:9px;overflow:hidden;border:1px solid var(--sl-line);cursor:pointer}
1192
+ .sl-confirm-thumb{display:block;width:100%;height:100%;object-fit:cover}
1193
+ .sl-confirm-thumb-badge{position:absolute;right:7px;top:7px;display:inline-flex;align-items:center;gap:5px;
1194
+ 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)}
1195
+ .sl-confirm-sight{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--sl-muted);margin-bottom:2px}
1196
+ .sl-confirm-sight span{color:#22a06b;font-weight:800}
873
1197
  .sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
874
1198
  color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
875
1199
  .sl-confirm-view:hover{border-color:var(--sl-muted)}
@@ -953,6 +1277,22 @@ function resolveTokens(chart, host) {
953
1277
  "--sl-radius": `${host?.radius ?? 14}px`
954
1278
  };
955
1279
  }
1280
+ var CB_STORAGE_KEY = "seatmap.a11y.cb";
1281
+ function readStoredColorblind() {
1282
+ try {
1283
+ if (typeof window === "undefined") return null;
1284
+ const raw = window.localStorage.getItem(CB_STORAGE_KEY);
1285
+ return raw == null ? null : raw === "1";
1286
+ } catch {
1287
+ return null;
1288
+ }
1289
+ }
1290
+ function writeStoredColorblind(on) {
1291
+ try {
1292
+ window.localStorage.setItem(CB_STORAGE_KEY, on ? "1" : "0");
1293
+ } catch {
1294
+ }
1295
+ }
956
1296
  var SeatPicker = class _SeatPicker {
957
1297
  constructor(options) {
958
1298
  this.root = null;
@@ -985,11 +1325,17 @@ var SeatPicker = class _SeatPicker {
985
1325
  this.confirmEl = null;
986
1326
  this.confirmSeat = null;
987
1327
  this.srEl = null;
988
- this.a11yFilter = "all";
989
1328
  this.baQty = 2;
990
1329
  this.baCat = "";
991
1330
  this.bestAvailableConfirm = false;
992
1331
  this.releasingHold = false;
1332
+ /** Event sales window is closed (read-only load state / live close). */
1333
+ this.salesClosed = false;
1334
+ /** Every seated category's live availability is 0 (sold-out overlay is up). */
1335
+ this.soldOut = false;
1336
+ this.soldoutEl = null;
1337
+ /** Resolved colorblind-safe state — stored preference wins over the option. */
1338
+ this.cbSafe = false;
993
1339
  // arena / multi-floor / seat-view chrome
994
1340
  this.rungsEl = null;
995
1341
  this.floorsEl = null;
@@ -998,13 +1344,11 @@ var SeatPicker = class _SeatPicker {
998
1344
  this.viewCleanup = null;
999
1345
  this.allSeatsCache = null;
1000
1346
  // F3 minimap
1001
- this.miniEl = null;
1002
1347
  this.miniCanvas = null;
1003
1348
  this.miniBase = null;
1004
1349
  this.miniTf = null;
1005
1350
  // F4 price-band filter — active band's category keys (null = all prices)
1006
1351
  this.priceBandKeys = null;
1007
- this.priceFilterEl = null;
1008
1352
  /** Last surfaced section summary (re-rendered when the price band changes). */
1009
1353
  this.lastSection = null;
1010
1354
  /** Section card collapsed to its slim pill (seat-picking has begun). */
@@ -1024,6 +1368,9 @@ var SeatPicker = class _SeatPicker {
1024
1368
  this.ctaPhase = "idle";
1025
1369
  // narrow-layout chrome that docks into the sheet's Filters row on mobile
1026
1370
  this.a11yChipsEl = null;
1371
+ this.fsFallback = false;
1372
+ this.fsChangeHandler = null;
1373
+ this.fsEscHandler = null;
1027
1374
  this.cbEl = null;
1028
1375
  // modal plumbing (set by open())
1029
1376
  this.modalScrim = null;
@@ -1031,20 +1378,22 @@ var SeatPicker = class _SeatPicker {
1031
1378
  this.escHandler = null;
1032
1379
  /** Set by open(): closes the modal (scroll restore + destroy + onClose). */
1033
1380
  this.closeModal = null;
1381
+ this.lastCatAvail = null;
1034
1382
  if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
1035
1383
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
1036
1384
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
1037
1385
  this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
1038
1386
  this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
1039
- this.api = new PubApi(this.apiBase);
1387
+ this.api = options.transport ?? new PubApi(this.apiBase);
1040
1388
  this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
1389
+ this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
1041
1390
  this.controller = new PickerController2({
1042
1391
  transport: this.api,
1043
1392
  eventKey: options.event,
1044
1393
  maxSelection: this.maxTickets,
1045
1394
  currency: options.currency,
1046
1395
  flashOnLiveChange: true,
1047
- colorblindSafe: options.colorblindSafe,
1396
+ colorblindSafe: this.cbSafe,
1048
1397
  onSelectionChange: () => {
1049
1398
  this.syncTray();
1050
1399
  if (this.committedSelection().length) this.collapseSectionCard();
@@ -1070,6 +1419,11 @@ var SeatPicker = class _SeatPicker {
1070
1419
  },
1071
1420
  confirmSelection: this.opts.confirmSelection,
1072
1421
  onSelect: (seat) => {
1422
+ if (this.salesClosed) {
1423
+ this.controller.deselect([seat.id]);
1424
+ this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
1425
+ return;
1426
+ }
1073
1427
  this.flashPickedSeat(seat.id);
1074
1428
  if (this.opts.confirmSelection) this.showConfirm(seat);
1075
1429
  },
@@ -1092,9 +1446,68 @@ var SeatPicker = class _SeatPicker {
1092
1446
  onHint: (m) => {
1093
1447
  if (m) this.toast(m);
1094
1448
  },
1449
+ // Server declared the event closed mid-session (409 event_closed) — keep
1450
+ // the toast (raised by handleCta), and add the persistent read-only state.
1451
+ onSalesClosed: () => this.setSalesClosed(true),
1095
1452
  onError: (err) => this.opts.onError?.(err)
1096
1453
  });
1097
1454
  }
1455
+ /**
1456
+ * Eager sightline preview for the confirm card: a cheap generated forward
1457
+ * view (or the organizer's real photo) plus a "Nm to stage · clear
1458
+ * sightline" line — the premium at-a-glance moment; click opens the 360.
1459
+ */
1460
+ confirmThumbHtml(seat) {
1461
+ const doc = this.controller.doc;
1462
+ if (!doc) return "";
1463
+ let url = seat.viewUrl ?? "";
1464
+ let distance = null;
1465
+ if (!url) {
1466
+ try {
1467
+ const thumb = generateSeatThumb(seat, doc.focalPoint);
1468
+ url = thumb.url;
1469
+ distance = thumb.distanceM ?? null;
1470
+ } catch {
1471
+ return "";
1472
+ }
1473
+ }
1474
+ const sight = distance != null ? `${distance}${this.tf("picker.sightline", "m to stage \xB7 clear sightline")}` : this.tf("picker.sightlineClear", "Clear sightline");
1475
+ return `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${t2("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>`;
1476
+ }
1477
+ /** Full screen via the native API, falling back to a fixed-position overlay (iOS Safari). */
1478
+ toggleFullscreen() {
1479
+ const root = this.root;
1480
+ if (!root) return;
1481
+ const active = !!document.fullscreenElement || this.fsFallback;
1482
+ if (!active) {
1483
+ if (root.requestFullscreen) {
1484
+ root.requestFullscreen().catch(() => this.setFsFallback(true));
1485
+ } else {
1486
+ this.setFsFallback(true);
1487
+ }
1488
+ } else if (document.fullscreenElement) {
1489
+ void document.exitFullscreen().catch(() => {
1490
+ });
1491
+ } else {
1492
+ this.setFsFallback(false);
1493
+ }
1494
+ }
1495
+ setFsFallback(on) {
1496
+ if (this.fsFallback === on) return;
1497
+ this.fsFallback = on;
1498
+ this.root?.classList.toggle("sl-fs", on);
1499
+ this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
1500
+ if (on && !this.fsEscHandler) {
1501
+ this.fsEscHandler = (e) => {
1502
+ if (e.key === "Escape" && !document.fullscreenElement) this.setFsFallback(false);
1503
+ };
1504
+ window.addEventListener("keydown", this.fsEscHandler);
1505
+ } else if (!on && this.fsEscHandler) {
1506
+ window.removeEventListener("keydown", this.fsEscHandler);
1507
+ this.fsEscHandler = null;
1508
+ }
1509
+ requestAnimationFrame(() => this.controller.zoomToFit());
1510
+ }
1098
1511
  /**
1099
1512
  * Close the picker. In modal mode (SeatPicker.open()) this dismisses the
1100
1513
  * modal exactly like ESC/scrim/✕ — restores page scroll and fires onClose.
@@ -1189,6 +1602,10 @@ var SeatPicker = class _SeatPicker {
1189
1602
  <div class="sl-head-meta" data-ref="meta"></div>
1190
1603
  </div>
1191
1604
  <span class="sl-hold-pill" data-ref="hold"></span>
1605
+ <span class="sl-closed-pill" data-ref="closedPill" role="status">
1606
+ <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>
1607
+ <span data-ref="closedPillText"></span>
1608
+ </span>
1192
1609
  <button type="button" class="sl-close" data-ref="close" aria-label="Close">
1193
1610
  <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>
1194
1611
  </button>
@@ -1202,6 +1619,9 @@ var SeatPicker = class _SeatPicker {
1202
1619
  <button type="button" aria-label="Fit to screen" data-ref="zfit">
1203
1620
  <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>
1204
1621
  </button>
1622
+ <button type="button" aria-label="Full screen" aria-pressed="false" data-ref="zfs">
1623
+ <svg viewBox="0 0 24 24"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
1624
+ </button>
1205
1625
  </div>
1206
1626
  <div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
1207
1627
  <div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
@@ -1220,6 +1640,7 @@ var SeatPicker = class _SeatPicker {
1220
1640
  <div class="sl-filters" data-ref="filters"></div>
1221
1641
  <div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>Ticket prices</span></div>
1222
1642
  <div class="sl-prices" data-ref="prices"></div>
1643
+ <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>
1223
1644
  <div class="sl-sec sl-seats-sec"><span>Your seats</span><span class="sl-seat-summary" data-ref="seatSummary"></span></div>
1224
1645
  <div class="sl-tray" data-ref="tray"></div>
1225
1646
  <div class="sl-foot" data-ref="foot">
@@ -1253,6 +1674,13 @@ var SeatPicker = class _SeatPicker {
1253
1674
  this.els.zin.addEventListener("click", () => this.controller.zoomIn());
1254
1675
  this.els.zout.addEventListener("click", () => this.controller.zoomOut());
1255
1676
  this.els.zfit.addEventListener("click", () => this.controller.zoomToFit());
1677
+ this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
1678
+ this.fsChangeHandler = () => {
1679
+ if (!document.fullscreenElement) this.setFsFallback(false);
1680
+ this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback));
1681
+ requestAnimationFrame(() => this.controller.zoomToFit());
1682
+ };
1683
+ document.addEventListener("fullscreenchange", this.fsChangeHandler);
1256
1684
  const head = this.els.sheetHead;
1257
1685
  if (head) {
1258
1686
  const toggle = this.els.sheetToggle;
@@ -1296,7 +1724,7 @@ var SeatPicker = class _SeatPicker {
1296
1724
  }
1297
1725
  this.tipEl = document.createElement("div");
1298
1726
  this.tipEl.setAttribute("role", "tooltip");
1299
- 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;";
1727
+ this.tipEl.className = "sl-tip";
1300
1728
  this.els.map.appendChild(this.tipEl);
1301
1729
  this.els.map.addEventListener("mousemove", (e) => {
1302
1730
  const r = this.els.map.getBoundingClientRect();
@@ -1321,6 +1749,7 @@ var SeatPicker = class _SeatPicker {
1321
1749
  return this;
1322
1750
  }
1323
1751
  this.els.boot.remove();
1752
+ this.salesClosed = !!info.salesClosed;
1324
1753
  this.buildRegions();
1325
1754
  this.regions["bottom-right"].appendChild(this.els.zoom);
1326
1755
  this.regions["bottom-center"].appendChild(this.els.toast);
@@ -1340,6 +1769,7 @@ var SeatPicker = class _SeatPicker {
1340
1769
  this.els.name.textContent = info.eventName ?? "";
1341
1770
  const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
1342
1771
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
1772
+ this.buildBadge(chartTheme);
1343
1773
  const present = /* @__PURE__ */ new Set();
1344
1774
  if (this.controller.doc) {
1345
1775
  for (const seat of expandChart(this.controller.doc)) {
@@ -1355,12 +1785,29 @@ var SeatPicker = class _SeatPicker {
1355
1785
  chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
1356
1786
  this.regions["top-left"].appendChild(chips);
1357
1787
  this.a11yChipsEl = chips;
1788
+ const active = /* @__PURE__ */ new Set();
1789
+ const syncChips = () => {
1790
+ chips.querySelectorAll("button").forEach((b) => {
1791
+ const f = b.dataset.f;
1792
+ const on = f === "all" ? active.size === 0 : active.has(f);
1793
+ b.classList.toggle("on", on);
1794
+ b.setAttribute("aria-pressed", String(on));
1795
+ });
1796
+ const filter = active.size ? [...active] : null;
1797
+ this.controller.setAccessibilityFilter(filter);
1798
+ if (filter && this.rungsEl && this.controller.getRung() !== "seats") {
1799
+ this.controller.setRung("seats");
1800
+ this.collapseSectionCard();
1801
+ this.syncRung();
1802
+ }
1803
+ };
1358
1804
  chips.querySelectorAll("button").forEach((btn) => {
1359
1805
  btn.addEventListener("click", () => {
1360
1806
  const f = btn.dataset.f;
1361
- this.a11yFilter = f;
1362
- chips.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
1363
- this.controller.setAccessibilityFilter(f === "all" ? null : [f]);
1807
+ if (f === "all") active.clear();
1808
+ else if (active.has(f)) active.delete(f);
1809
+ else active.add(f);
1810
+ syncChips();
1364
1811
  });
1365
1812
  });
1366
1813
  }
@@ -1369,14 +1816,14 @@ var SeatPicker = class _SeatPicker {
1369
1816
  cb.className = "sl-cbbtn";
1370
1817
  this.cbEl = cb;
1371
1818
  cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
1372
- cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
1819
+ cb.setAttribute("aria-pressed", String(this.cbSafe));
1373
1820
  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>';
1374
1821
  this.els.zfit.parentElement.appendChild(cb);
1375
- let cbOn = !!this.opts.colorblindSafe;
1376
1822
  cb.addEventListener("click", () => {
1377
- cbOn = !cbOn;
1378
- cb.setAttribute("aria-pressed", String(cbOn));
1379
- this.controller.setColorblindSafe(cbOn);
1823
+ this.cbSafe = !this.cbSafe;
1824
+ cb.setAttribute("aria-pressed", String(this.cbSafe));
1825
+ this.controller.setColorblindSafe(this.cbSafe);
1826
+ writeStoredColorblind(this.cbSafe);
1380
1827
  });
1381
1828
  this.srEl = document.createElement("div");
1382
1829
  this.srEl.className = "sl-sr";
@@ -1387,9 +1834,11 @@ var SeatPicker = class _SeatPicker {
1387
1834
  this.buildPriceFilter();
1388
1835
  this.buildExtendPrompt();
1389
1836
  this.buildBookedOverlay();
1837
+ this.buildSoldoutOverlay();
1390
1838
  this.dockLayoutChrome();
1391
1839
  await this.restoreRememberedHold();
1392
1840
  if (this.destroyed) return this;
1841
+ if (this.salesClosed) this.applySalesClosed();
1393
1842
  this.syncPrices();
1394
1843
  this.syncTray();
1395
1844
  return this;
@@ -1441,6 +1890,85 @@ var SeatPicker = class _SeatPicker {
1441
1890
  this.bookedEl = el;
1442
1891
  this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
1443
1892
  }
1893
+ /**
1894
+ * Localized string with a literal fallback. `t()` returns the key itself for
1895
+ * unknown keys, so this collapses that to `fallback` — while still honoring a
1896
+ * host `messages` override (which makes `t()` return the override, not the key).
1897
+ */
1898
+ tf(key, fallback) {
1899
+ const v = t2(key);
1900
+ return v === key ? fallback : v;
1901
+ }
1902
+ /** Sold-out overlay — centered over the map, disabled waitlist stub (Gap 2). */
1903
+ buildSoldoutOverlay() {
1904
+ if (!this.els.map) return;
1905
+ const el = document.createElement("div");
1906
+ el.className = "sl-soldout";
1907
+ el.setAttribute("role", "status");
1908
+ const name = (this.controller.doc?.theme?.brandName ?? this.opts.theme?.brandName ?? this.els.name?.textContent ?? this.tf("picker.soldOutEyebrow", "This event")).toUpperCase();
1909
+ 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>`;
1910
+ this.els.map.appendChild(el);
1911
+ this.soldoutEl = el;
1912
+ }
1913
+ /**
1914
+ * Recompute the sold-out state on every price/availability sync. Sold-out ⇔
1915
+ * every SEATED category's live free count is 0. Suppressed when the chart has
1916
+ * GA areas (GA capacity isn't per-seat, so seated counts would read 0 and
1917
+ * falsely block standing room) — mirrors the public page. Clears live when WS
1918
+ * frees a seat up.
1919
+ */
1920
+ syncSoldout(categories, left) {
1921
+ const hasGA = this.controller.getGAAreas().length > 0;
1922
+ const soldOut = this.isSoldOut(categories, left, hasGA);
1923
+ if (soldOut === this.soldOut) return;
1924
+ this.soldOut = soldOut;
1925
+ this.soldoutEl?.classList.toggle("on", soldOut);
1926
+ }
1927
+ /**
1928
+ * Pure sold-out predicate: every SEATED category's free count is 0, there is at
1929
+ * least one seated category, and there are no GA areas (GA capacity isn't
1930
+ * per-seat, so seated counts read 0 and would falsely block standing room).
1931
+ * `left` is seeded implicitly — a missing key means a fully-booked tier (0 free).
1932
+ */
1933
+ isSoldOut(categories, left, hasGA) {
1934
+ return !hasGA && categories.length > 0 && categories.every((c) => (left[c.key] ?? 0) === 0);
1935
+ }
1936
+ /**
1937
+ * Sales-closed read-only state (Gap 3): persistent header pill, disabled CTA
1938
+ * with a closed label, and frozen best-available / GA controls. `setSalesClosed`
1939
+ * is the reactive entry (live 409 event_closed); `applySalesClosed` is the
1940
+ * idempotent DOM apply used at load and on transition.
1941
+ */
1942
+ setSalesClosed(closed) {
1943
+ if (this.salesClosed === closed) return;
1944
+ this.salesClosed = closed;
1945
+ this.applySalesClosed();
1946
+ }
1947
+ applySalesClosed() {
1948
+ const pill = this.els.closedPill;
1949
+ if (pill) {
1950
+ pill.classList.toggle("on", this.salesClosed);
1951
+ const text = this.els.closedPillText ?? pill;
1952
+ text.textContent = this.tf("picker.salesClosedPill", "Sales are closed");
1953
+ }
1954
+ this.root?.setAttribute("data-sales-closed", String(this.salesClosed));
1955
+ this.syncCta();
1956
+ this.syncTray();
1957
+ }
1958
+ /** The badge is hidden when the host opts out OR the org's theme sets hideBadge. */
1959
+ badgeHidden(chartTheme) {
1960
+ return !!(this.opts.hideBadge || chartTheme?.hideBadge);
1961
+ }
1962
+ /** Attribution badge in the side-panel foot (Gap 7). Hidden per host/theme. */
1963
+ buildBadge(chartTheme) {
1964
+ if (this.badgeHidden(chartTheme)) return;
1965
+ const foot = this.els.foot;
1966
+ if (!foot) return;
1967
+ const el = document.createElement("div");
1968
+ el.className = "sl-powered";
1969
+ 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>`;
1970
+ foot.appendChild(el);
1971
+ }
1444
1972
  // ---- Feature 6: chrome anchor regions -------------------------------------
1445
1973
  /**
1446
1974
  * Create the positioned flex containers that own every persistent map overlay.
@@ -1553,13 +2081,18 @@ var SeatPicker = class _SeatPicker {
1553
2081
  heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
1554
2082
  }
1555
2083
  return gaAreas.reduce(
1556
- (sum, area) => sum + area.price * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
2084
+ (sum, area) => sum + this.paidPrice(area.categoryKey, null, area.price) * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
1557
2085
  0
1558
2086
  );
1559
2087
  }
1560
2088
  syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
1561
2089
  const cta = this.els.cta;
1562
2090
  if (!cta) return;
2091
+ if (this.salesClosed) {
2092
+ cta.disabled = true;
2093
+ cta.textContent = this.tf("picker.salesClosedCta", "Sales closed");
2094
+ return;
2095
+ }
1563
2096
  if (this.confirmSeat) {
1564
2097
  cta.disabled = true;
1565
2098
  cta.textContent = "Confirm or cancel this seat";
@@ -1695,7 +2228,6 @@ var SeatPicker = class _SeatPicker {
1695
2228
  canvas.style.height = `${h}px`;
1696
2229
  wrap.appendChild(canvas);
1697
2230
  (this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
1698
- this.miniEl = wrap;
1699
2231
  this.miniCanvas = canvas;
1700
2232
  const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
1701
2233
  const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
@@ -1808,9 +2340,11 @@ var SeatPicker = class _SeatPicker {
1808
2340
  this.controller.overview();
1809
2341
  }
1810
2342
  // ---- F4 price-band filter -------------------------------------------------
1811
- /** Effective price of a category (first tier when tiered, else base price). */
2343
+ /** Effective display price of a category: host pricing override → first tier base. */
1812
2344
  catPrice(c) {
1813
- return c.tiers?.length ? c.tiers[0].price : c.price;
2345
+ const chart = c.tiers?.length ? c.tiers[0].price : c.price;
2346
+ if (chart === void 0 || !c.key) return chart;
2347
+ return this.paidPrice(c.key, c.tiers?.[0]?.id ?? null, chart);
1814
2348
  }
1815
2349
  /** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
1816
2350
  priceBands() {
@@ -1855,7 +2389,6 @@ var SeatPicker = class _SeatPicker {
1855
2389
  select.setAttribute("aria-label", "Filter and focus seats by price");
1856
2390
  select.innerHTML = `<option value="all">All prices</option>` + bands.map((band) => `<option value="${band.id}">${band.label}</option>`).join("");
1857
2391
  this.els.pricesSec.appendChild(select);
1858
- this.priceFilterEl = select;
1859
2392
  select.addEventListener("change", () => {
1860
2393
  const band = bands.find((candidate) => candidate.id === select.value);
1861
2394
  const keys = band?.keys ?? null;
@@ -1959,7 +2492,10 @@ var SeatPicker = class _SeatPicker {
1959
2492
  renderSectionCard(summary) {
1960
2493
  if (!this.els.map) return;
1961
2494
  this.secCardEl?.remove();
1962
- const priceLabel = summary.priceMin === summary.priceMax ? this.money(summary.priceMin) : `${this.money(summary.priceMin)}\u2013${this.money(summary.priceMax)}`;
2495
+ const paid = summary.categories.length ? summary.categories.map((c) => this.paidPrice(c.key, null, c.price)) : [summary.priceMin, summary.priceMax];
2496
+ const paidMin = Math.min(...paid);
2497
+ const paidMax = Math.max(...paid);
2498
+ const priceLabel = paidMin === paidMax ? this.money(paidMin) : `${this.money(paidMin)}\u2013${this.money(paidMax)}`;
1963
2499
  const leftLabel = tCount("picker.seatsLeftInSection", summary.seatsLeft);
1964
2500
  const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${t2("picker.closeSectionSummary")}">\u2715</button>`;
1965
2501
  const card = document.createElement("div");
@@ -1989,7 +2525,7 @@ var SeatPicker = class _SeatPicker {
1989
2525
  card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
1990
2526
  const mix = summary.categories.map((c) => {
1991
2527
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
1992
- 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>`;
2528
+ 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>`;
1993
2529
  }).join("");
1994
2530
  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 ${t2("picker.overview")}</button><span class="sl-seccard-hint">${t2("picker.tapSeatHint")}</span></div>`;
1995
2531
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
@@ -2056,7 +2592,7 @@ var SeatPicker = class _SeatPicker {
2056
2592
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
2057
2593
  const status = this.controller.getStatus(seat.id) ?? "free";
2058
2594
  const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
2059
- const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
2595
+ const price = cat ? this.catPrice(cat) : void 0;
2060
2596
  this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
2061
2597
  }
2062
2598
  // ---- seat candidate confirmation ------------------------------------------
@@ -2071,7 +2607,8 @@ var SeatPicker = class _SeatPicker {
2071
2607
  if (this.tipEl) this.tipEl.style.display = "none";
2072
2608
  const details = this.controller.seatDetails(seat.id);
2073
2609
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
2074
- const price = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
2610
+ const chartPrice = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
2611
+ const price = chartPrice != null ? this.paidPrice(seat.categoryKey, details?.tierId ?? cat?.tiers?.[0]?.id ?? null, chartPrice) : void 0;
2075
2612
  const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
2076
2613
  "&": "&amp;",
2077
2614
  "<": "&lt;",
@@ -2084,7 +2621,7 @@ var SeatPicker = class _SeatPicker {
2084
2621
  el.setAttribute("aria-modal", "true");
2085
2622
  el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
2086
2623
  el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
2087
- 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>${t2("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>`;
2624
+ 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>`;
2088
2625
  this.els.map.appendChild(el);
2089
2626
  this.confirmEl = el;
2090
2627
  this.reanchorConfirm();
@@ -2249,18 +2786,35 @@ var SeatPicker = class _SeatPicker {
2249
2786
  }
2250
2787
  // ---- chrome sync ----------------------------------------------------------
2251
2788
  money(n) {
2789
+ const formatter = this.opts.pricing?.formatter;
2790
+ if (formatter) return formatter(n, this.currency);
2252
2791
  try {
2253
2792
  return new Intl.NumberFormat(this.opts.locale, { style: "currency", currency: this.currency }).format(n);
2254
2793
  } catch {
2255
2794
  return `${n} ${this.currency}`;
2256
2795
  }
2257
2796
  }
2797
+ /**
2798
+ * The price the buyer will actually pay for a category (+tier): the host's
2799
+ * `pricing` override when present, else the chart's stored price. Every
2800
+ * price the widget DISPLAYS or hands off must flow through here — a map
2801
+ * that shows one price while checkout charges another destroys trust.
2802
+ */
2803
+ paidPrice(categoryKey, tierId, fallback) {
2804
+ const entry = categoryKey ? this.opts.pricing?.prices?.[categoryKey] : void 0;
2805
+ if (entry === void 0) return fallback;
2806
+ if (typeof entry === "number") return entry;
2807
+ if (tierId && entry.tiers?.[tierId] !== void 0) return entry.tiers[tierId];
2808
+ return entry.base ?? fallback;
2809
+ }
2258
2810
  syncPrices() {
2259
2811
  const doc = this.controller.doc;
2260
2812
  if (!doc || !this.els.prices) return;
2261
2813
  const left = this.controller.categoryAvailability();
2814
+ this.narrateAvailability(doc.categories, left);
2815
+ this.syncSoldout(doc.categories, left);
2262
2816
  this.els.prices.innerHTML = doc.categories.map((c) => {
2263
- const price = c.tiers?.length ? c.tiers[0].price : c.price;
2817
+ const price = this.catPrice(c);
2264
2818
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2265
2819
  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>`;
2266
2820
  }).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>`;
@@ -2269,6 +2823,26 @@ var SeatPicker = class _SeatPicker {
2269
2823
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
2270
2824
  });
2271
2825
  }
2826
+ /**
2827
+ * Live-activity strip: turn WS availability deltas into one quiet line of
2828
+ * social proof ("2 seats just taken in VIP · 118 left"). Diffs per-category
2829
+ * counts on every status change — no per-seat payload needed. Skips the very
2830
+ * first computation (initial load is not "activity").
2831
+ */
2832
+ narrateAvailability(categories, left) {
2833
+ const textEl = this.els.liveText;
2834
+ const prev = this.lastCatAvail;
2835
+ this.lastCatAvail = { ...left };
2836
+ if (!textEl || !prev) return;
2837
+ for (const cat of categories) {
2838
+ const before = prev[cat.key];
2839
+ const now = left[cat.key] ?? 0;
2840
+ if (before === void 0 || now >= before) continue;
2841
+ const taken = before - now;
2842
+ textEl.textContent = `${taken} seat${taken === 1 ? "" : "s"} just taken in ${cat.label} \xB7 ${now} left`;
2843
+ return;
2844
+ }
2845
+ }
2272
2846
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
2273
2847
  evictTakenSelections() {
2274
2848
  const ownLabels = /* @__PURE__ */ new Set([
@@ -2297,15 +2871,23 @@ var SeatPicker = class _SeatPicker {
2297
2871
  const cats = this.controller.doc?.categories ?? [];
2298
2872
  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>`);
2299
2873
  }
2874
+ const idGrid = (seatId, label) => {
2875
+ const d = seatId ? this.controller.seatDetails(seatId) : null;
2876
+ if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
2877
+ return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
2878
+ }
2879
+ 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>`;
2880
+ };
2881
+ const iconRail = (rmAria, viewLabel) => `<div class="sl-chip-rail"><button type="button" class="rm" aria-label="${rmAria}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>` + (viewLabel ? `<button type="button" class="view" data-view-label="${viewLabel}" aria-label="${t2("picker.viewFromSeat", { label: viewLabel })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") + `</div>`;
2300
2882
  for (const item of heldItems) {
2301
2883
  const itemKey = `held:${item.label}`;
2302
2884
  nextTrayKeys.add(itemKey);
2303
2885
  const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
2304
2886
  const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
2305
- const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
2306
- const viewBtn = canView2 ? `<button type="button" class="view" data-view-label="${item.label}" aria-label="${t2("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>` : "";
2887
+ const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
2888
+ const canView2 = this.seatViewEnabled() && !!heldSeat;
2307
2889
  parts.push(
2308
- `<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>`
2890
+ `<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>`
2309
2891
  );
2310
2892
  }
2311
2893
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -2314,16 +2896,15 @@ var SeatPicker = class _SeatPicker {
2314
2896
  const itemKey = `seat:${s.id}`;
2315
2897
  nextTrayKeys.add(itemKey);
2316
2898
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
2317
- const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(ti.price)}</option>`).join("") + `</select>` : "";
2318
- const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${t2("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>` : "";
2899
+ const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(this.paidPrice(s.categoryKey, ti.id, ti.price))}</option>`).join("") + `</select>` : "";
2319
2900
  parts.push(
2320
- `<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>`
2901
+ `<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>`
2321
2902
  );
2322
2903
  }
2323
2904
  for (const area of gaAreas) {
2324
2905
  const qty = this.gaQty.get(area.id) ?? 0;
2325
2906
  parts.push(
2326
- `<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>`
2907
+ `<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>`
2327
2908
  );
2328
2909
  }
2329
2910
  this.els.tray.innerHTML = parts.join("");
@@ -2363,7 +2944,7 @@ var SeatPicker = class _SeatPicker {
2363
2944
  return;
2364
2945
  }
2365
2946
  const id = chip.dataset.seat;
2366
- const label = chip.querySelector("b")?.textContent ?? "Seat";
2947
+ const label = this.controller.getSelection().find((sel) => sel.id === id)?.label ?? "Seat";
2367
2948
  const remove = () => {
2368
2949
  this.controller.deselect([id]);
2369
2950
  this.toast(`${label} removed.`, "neutral", {
@@ -2394,6 +2975,11 @@ var SeatPicker = class _SeatPicker {
2394
2975
  if (seat) this.openSeatView(seat);
2395
2976
  });
2396
2977
  });
2978
+ this.els.tray.querySelectorAll(".sl-chip[data-locate]").forEach((chip) => {
2979
+ const locate = () => this.controller.flashSeat(chip.dataset.locate, this.cssVar("--sl-accent") || "#f4b740");
2980
+ chip.addEventListener("mouseenter", locate);
2981
+ chip.addEventListener("focusin", locate);
2982
+ });
2397
2983
  this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
2398
2984
  btn.addEventListener("click", () => {
2399
2985
  const areaEl = btn.closest(".sl-ga");
@@ -2406,12 +2992,17 @@ var SeatPicker = class _SeatPicker {
2406
2992
  this.syncTray();
2407
2993
  });
2408
2994
  });
2995
+ if (this.salesClosed) {
2996
+ this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-replace],.sl-ga button").forEach((el) => {
2997
+ el.disabled = true;
2998
+ });
2999
+ }
2409
3000
  const gaTotal = this.pendingGATotal(gaAreas);
2410
3001
  const gaCount = this.pendingGACount();
2411
- const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
3002
+ const heldTotal = heldItems.reduce((sum, item) => sum + this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1), 0);
2412
3003
  const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
2413
3004
  const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
2414
- const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
3005
+ const total = freshSeats.reduce((sum, s) => sum + this.paidPrice(s.categoryKey, s.tierId ?? null, s.price), 0) + gaTotal + heldTotal;
2415
3006
  const count = freshSeats.length + gaCount + heldCount;
2416
3007
  const pendingCount = this.pendingSelectionCount();
2417
3008
  const previousCount = this.lastTrayCount;
@@ -2507,6 +3098,7 @@ var SeatPicker = class _SeatPicker {
2507
3098
  }
2508
3099
  }
2509
3100
  async handleCta() {
3101
+ if (this.salesClosed) return;
2510
3102
  if (this.totalTicketCount() > this.maxTickets) {
2511
3103
  this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
2512
3104
  return;
@@ -2550,6 +3142,7 @@ var SeatPicker = class _SeatPicker {
2550
3142
  this.opts.onError?.(err);
2551
3143
  const problem = err;
2552
3144
  const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
3145
+ if (problem.reason === "event_closed") this.setSalesClosed(true);
2553
3146
  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.";
2554
3147
  this.toast(message, "error");
2555
3148
  this.setCtaPhase("idle");
@@ -2658,7 +3251,7 @@ var SeatPicker = class _SeatPicker {
2658
3251
  objectType: it.objectType,
2659
3252
  categoryKey: it.categoryKey,
2660
3253
  tierId: it.tierId,
2661
- unitPrice: it.unitPrice,
3254
+ unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
2662
3255
  currency: it.currency ?? this.currency,
2663
3256
  quantity: it.quantity ?? 1
2664
3257
  }));
@@ -2711,19 +3304,37 @@ var SeatPicker = class _SeatPicker {
2711
3304
  this.tipEl.style.left = `${Math.max(8, x)}px`;
2712
3305
  this.tipEl.style.top = `${Math.max(8, y)}px`;
2713
3306
  }
3307
+ /**
3308
+ * Row label without the redundant section prefix. Charts commonly name row
3309
+ * objects "104-A" while the Section column already shows "104" — so the Row
3310
+ * cell repeats the section and, in the compact hover card, truncates to
3311
+ * "10…". Strip a leading "<section><sep>" so Row reads a clean "A". Only when
3312
+ * the prefix is exact (won't touch "1040-A" under section "104"); otherwise
3313
+ * the label is shown verbatim.
3314
+ */
3315
+ rowShort(details) {
3316
+ const row = details?.rowLabel;
3317
+ const sec = details?.sectionLabel;
3318
+ if (!row || !sec) return row;
3319
+ for (const sep of ["-", " ", "\xB7", "/", "_"]) {
3320
+ const prefix = `${sec}${sep}`;
3321
+ if (row.startsWith(prefix) && row.length > prefix.length) return row.slice(prefix.length);
3322
+ }
3323
+ return row;
3324
+ }
2714
3325
  updateTooltip(details) {
2715
3326
  if (!this.tipEl) return;
2716
3327
  if (!details) {
2717
3328
  this.tipEl.style.display = "none";
2718
3329
  return;
2719
3330
  }
2720
- 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" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
2721
- const location = [
2722
- details.sectionLabel ? `Section ${details.sectionLabel}` : "",
2723
- details.rowLabel ? `Row ${details.rowLabel}` : "",
2724
- details.seatNumber ? `Seat ${details.seatNumber}` : details.label
2725
- ].filter(Boolean).join(" \xB7 ");
2726
- 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;
3331
+ const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
3332
+ const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
3333
+ const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
3334
+ 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>`;
3335
+ const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
3336
+ this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
3337
+ 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;
2727
3338
  this.tipEl.style.display = "block";
2728
3339
  this.placeTooltip();
2729
3340
  }
@@ -2744,7 +3355,7 @@ var SeatPicker = class _SeatPicker {
2744
3355
  return this.removeHeldLabel(label);
2745
3356
  }
2746
3357
  async bestAvailable(qty, categoryKey) {
2747
- if (this.bestAvailableBusy) return null;
3358
+ if (this.salesClosed || this.bestAvailableBusy) return null;
2748
3359
  qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
2749
3360
  if (this.confirmSeat) this.cancelConfirm();
2750
3361
  this.bestAvailableConfirm = false;
@@ -2825,6 +3436,8 @@ var SeatPicker = class _SeatPicker {
2825
3436
  this.ro?.disconnect();
2826
3437
  this.ro = null;
2827
3438
  if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
3439
+ if (this.fsChangeHandler) document.removeEventListener("fullscreenchange", this.fsChangeHandler);
3440
+ if (this.fsEscHandler) window.removeEventListener("keydown", this.fsEscHandler);
2828
3441
  this.controller.destroy();
2829
3442
  this.root?.remove();
2830
3443
  this.root = null;