@seatlayer/js 0.18.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
@@ -850,26 +850,43 @@ var CSS = `
850
850
  .sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
851
851
  overscroll-behavior:contain;scrollbar-gutter:stable}
852
852
  .sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
853
- .sl-chip{position:relative;display:grid;grid-template-columns:24px minmax(0,1fr) auto 30px;align-items:center;gap:8px;
854
- 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;
855
855
  background:var(--sl-surface);font-size:13px;transform-origin:center;transition:border-color .15s,background .15s}
856
856
  .sl-chip:hover{border-color:color-mix(in srgb,var(--sl-accent) 38%,var(--sl-line))}
857
857
  .sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
858
858
  .sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
859
859
  .sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
860
860
  box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
861
- .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;
862
862
  background:var(--sl-accent);color:var(--sl-accent-ink)}
863
863
  .sl-ticket-state.held{background:color-mix(in srgb,var(--sl-accent) 18%,var(--sl-surface));color:var(--sl-accent)}
864
- .sl-ticket-state svg{width:13px;height:13px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
865
- .sl-chip-main{min-width:0}
866
- .sl-chip b{display:block;font-weight:800;min-width:max-content;white-space:nowrap}
867
- .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}
868
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}
869
875
  .sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums;flex:none;white-space:nowrap}
870
- .sl-chip .rm{width:29px;height:29px;border-radius:8px;flex:none;display:flex;align-items:center;justify-content:center;color:var(--sl-muted)}
871
- .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)}
872
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}
873
890
 
874
891
  /* GA rows */
875
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)}
@@ -945,6 +962,8 @@ var CSS = `
945
962
 
946
963
  /* zoom column (flows within the bottom-right region) */
947
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}
948
967
  .sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
949
968
  color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
950
969
  .sl-zoom button:hover{border-color:var(--sl-muted)}
@@ -1003,6 +1022,39 @@ var CSS = `
1003
1022
  .sl-booked.on .sl-booked-title{animation-delay:.22s}
1004
1023
  .sl-booked.on .sl-booked-sub{animation-delay:.3s}
1005
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
+
1006
1058
  /* a11y filter chips (flow within the top-left region) */
1007
1059
  .sl-chips{display:flex;gap:6px;flex-wrap:wrap}
1008
1060
  .sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
@@ -1041,8 +1093,30 @@ var CSS = `
1041
1093
  .sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
1042
1094
  transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
1043
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
+
1044
1118
  /* Best available is a first-class shortcut, not an anonymous utility row. */
1045
- .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;
1046
1120
  padding:13px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));border-radius:13px;
1047
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)))}
1048
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}
@@ -1078,12 +1152,6 @@ var CSS = `
1078
1152
  /* per-seat ticket-tier select + view-from-seat button in tray chips */
1079
1153
  .sl-chip .tier{background:var(--sl-bg);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:6px;
1080
1154
  font:inherit;font-size:10px;padding:2px 4px;min-width:0;max-width:100%;cursor:pointer}
1081
- .sl-chip .view{width:20px;height:20px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
1082
- color:var(--sl-muted);opacity:.36;transition:color .15s,opacity .15s}
1083
- .sl-chip .view:hover{color:var(--sl-text)}
1084
- .sl-chip:hover .view,.sl-chip .view:focus-visible{opacity:1;color:var(--sl-text)}
1085
- .sl-chip .view svg{width:12px;height:12px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
1086
- @media(pointer:coarse){.sl-chip .view{opacity:.58}}
1087
1155
 
1088
1156
  /* arena: LOD rung pills (flow within the top-center region) */
1089
1157
  .sl-rungs{display:none;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
@@ -1141,6 +1209,14 @@ var CSS = `
1141
1209
  .sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
1142
1210
 
1143
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}
1144
1220
  .sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
1145
1221
  color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
1146
1222
  .sl-confirm-view:hover{border-color:var(--sl-muted)}
@@ -1224,6 +1300,22 @@ function resolveTokens(chart, host) {
1224
1300
  "--sl-radius": `${host?.radius ?? 14}px`
1225
1301
  };
1226
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
+ }
1227
1319
  var SeatPicker = class _SeatPicker {
1228
1320
  constructor(options) {
1229
1321
  this.root = null;
@@ -1256,11 +1348,17 @@ var SeatPicker = class _SeatPicker {
1256
1348
  this.confirmEl = null;
1257
1349
  this.confirmSeat = null;
1258
1350
  this.srEl = null;
1259
- this.a11yFilter = "all";
1260
1351
  this.baQty = 2;
1261
1352
  this.baCat = "";
1262
1353
  this.bestAvailableConfirm = false;
1263
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;
1264
1362
  // arena / multi-floor / seat-view chrome
1265
1363
  this.rungsEl = null;
1266
1364
  this.floorsEl = null;
@@ -1269,13 +1367,11 @@ var SeatPicker = class _SeatPicker {
1269
1367
  this.viewCleanup = null;
1270
1368
  this.allSeatsCache = null;
1271
1369
  // F3 minimap
1272
- this.miniEl = null;
1273
1370
  this.miniCanvas = null;
1274
1371
  this.miniBase = null;
1275
1372
  this.miniTf = null;
1276
1373
  // F4 price-band filter — active band's category keys (null = all prices)
1277
1374
  this.priceBandKeys = null;
1278
- this.priceFilterEl = null;
1279
1375
  /** Last surfaced section summary (re-rendered when the price band changes). */
1280
1376
  this.lastSection = null;
1281
1377
  /** Section card collapsed to its slim pill (seat-picking has begun). */
@@ -1295,6 +1391,9 @@ var SeatPicker = class _SeatPicker {
1295
1391
  this.ctaPhase = "idle";
1296
1392
  // narrow-layout chrome that docks into the sheet's Filters row on mobile
1297
1393
  this.a11yChipsEl = null;
1394
+ this.fsFallback = false;
1395
+ this.fsChangeHandler = null;
1396
+ this.fsEscHandler = null;
1298
1397
  this.cbEl = null;
1299
1398
  // modal plumbing (set by open())
1300
1399
  this.modalScrim = null;
@@ -1302,20 +1401,22 @@ var SeatPicker = class _SeatPicker {
1302
1401
  this.escHandler = null;
1303
1402
  /** Set by open(): closes the modal (scroll restore + destroy + onClose). */
1304
1403
  this.closeModal = null;
1404
+ this.lastCatAvail = null;
1305
1405
  if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
1306
1406
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
1307
1407
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
1308
1408
  this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
1309
1409
  this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
1310
- this.api = new PubApi(this.apiBase);
1410
+ this.api = options.transport ?? new PubApi(this.apiBase);
1311
1411
  this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
1412
+ this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
1312
1413
  this.controller = new import_core2.PickerController({
1313
1414
  transport: this.api,
1314
1415
  eventKey: options.event,
1315
1416
  maxSelection: this.maxTickets,
1316
1417
  currency: options.currency,
1317
1418
  flashOnLiveChange: true,
1318
- colorblindSafe: options.colorblindSafe,
1419
+ colorblindSafe: this.cbSafe,
1319
1420
  onSelectionChange: () => {
1320
1421
  this.syncTray();
1321
1422
  if (this.committedSelection().length) this.collapseSectionCard();
@@ -1341,6 +1442,11 @@ var SeatPicker = class _SeatPicker {
1341
1442
  },
1342
1443
  confirmSelection: this.opts.confirmSelection,
1343
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
+ }
1344
1450
  this.flashPickedSeat(seat.id);
1345
1451
  if (this.opts.confirmSelection) this.showConfirm(seat);
1346
1452
  },
@@ -1363,9 +1469,68 @@ var SeatPicker = class _SeatPicker {
1363
1469
  onHint: (m) => {
1364
1470
  if (m) this.toast(m);
1365
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),
1366
1475
  onError: (err) => this.opts.onError?.(err)
1367
1476
  });
1368
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
+ }
1369
1534
  /**
1370
1535
  * Close the picker. In modal mode (SeatPicker.open()) this dismisses the
1371
1536
  * modal exactly like ESC/scrim/✕ — restores page scroll and fires onClose.
@@ -1460,6 +1625,10 @@ var SeatPicker = class _SeatPicker {
1460
1625
  <div class="sl-head-meta" data-ref="meta"></div>
1461
1626
  </div>
1462
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>
1463
1632
  <button type="button" class="sl-close" data-ref="close" aria-label="Close">
1464
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>
1465
1634
  </button>
@@ -1473,6 +1642,9 @@ var SeatPicker = class _SeatPicker {
1473
1642
  <button type="button" aria-label="Fit to screen" data-ref="zfit">
1474
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>
1475
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>
1476
1648
  </div>
1477
1649
  <div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
1478
1650
  <div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
@@ -1491,6 +1663,7 @@ var SeatPicker = class _SeatPicker {
1491
1663
  <div class="sl-filters" data-ref="filters"></div>
1492
1664
  <div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>Ticket prices</span></div>
1493
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>
1494
1667
  <div class="sl-sec sl-seats-sec"><span>Your seats</span><span class="sl-seat-summary" data-ref="seatSummary"></span></div>
1495
1668
  <div class="sl-tray" data-ref="tray"></div>
1496
1669
  <div class="sl-foot" data-ref="foot">
@@ -1524,6 +1697,13 @@ var SeatPicker = class _SeatPicker {
1524
1697
  this.els.zin.addEventListener("click", () => this.controller.zoomIn());
1525
1698
  this.els.zout.addEventListener("click", () => this.controller.zoomOut());
1526
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);
1527
1707
  const head = this.els.sheetHead;
1528
1708
  if (head) {
1529
1709
  const toggle = this.els.sheetToggle;
@@ -1567,7 +1747,7 @@ var SeatPicker = class _SeatPicker {
1567
1747
  }
1568
1748
  this.tipEl = document.createElement("div");
1569
1749
  this.tipEl.setAttribute("role", "tooltip");
1570
- 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";
1571
1751
  this.els.map.appendChild(this.tipEl);
1572
1752
  this.els.map.addEventListener("mousemove", (e) => {
1573
1753
  const r = this.els.map.getBoundingClientRect();
@@ -1592,6 +1772,7 @@ var SeatPicker = class _SeatPicker {
1592
1772
  return this;
1593
1773
  }
1594
1774
  this.els.boot.remove();
1775
+ this.salesClosed = !!info.salesClosed;
1595
1776
  this.buildRegions();
1596
1777
  this.regions["bottom-right"].appendChild(this.els.zoom);
1597
1778
  this.regions["bottom-center"].appendChild(this.els.toast);
@@ -1611,6 +1792,7 @@ var SeatPicker = class _SeatPicker {
1611
1792
  this.els.name.textContent = info.eventName ?? "";
1612
1793
  const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
1613
1794
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
1795
+ this.buildBadge(chartTheme);
1614
1796
  const present = /* @__PURE__ */ new Set();
1615
1797
  if (this.controller.doc) {
1616
1798
  for (const seat of (0, import_core2.expandChart)(this.controller.doc)) {
@@ -1626,12 +1808,29 @@ var SeatPicker = class _SeatPicker {
1626
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("");
1627
1809
  this.regions["top-left"].appendChild(chips);
1628
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
+ };
1629
1827
  chips.querySelectorAll("button").forEach((btn) => {
1630
1828
  btn.addEventListener("click", () => {
1631
1829
  const f = btn.dataset.f;
1632
- this.a11yFilter = f;
1633
- chips.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
1634
- 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();
1635
1834
  });
1636
1835
  });
1637
1836
  }
@@ -1640,14 +1839,14 @@ var SeatPicker = class _SeatPicker {
1640
1839
  cb.className = "sl-cbbtn";
1641
1840
  this.cbEl = cb;
1642
1841
  cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
1643
- cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
1842
+ cb.setAttribute("aria-pressed", String(this.cbSafe));
1644
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>';
1645
1844
  this.els.zfit.parentElement.appendChild(cb);
1646
- let cbOn = !!this.opts.colorblindSafe;
1647
1845
  cb.addEventListener("click", () => {
1648
- cbOn = !cbOn;
1649
- cb.setAttribute("aria-pressed", String(cbOn));
1650
- 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);
1651
1850
  });
1652
1851
  this.srEl = document.createElement("div");
1653
1852
  this.srEl.className = "sl-sr";
@@ -1658,9 +1857,11 @@ var SeatPicker = class _SeatPicker {
1658
1857
  this.buildPriceFilter();
1659
1858
  this.buildExtendPrompt();
1660
1859
  this.buildBookedOverlay();
1860
+ this.buildSoldoutOverlay();
1661
1861
  this.dockLayoutChrome();
1662
1862
  await this.restoreRememberedHold();
1663
1863
  if (this.destroyed) return this;
1864
+ if (this.salesClosed) this.applySalesClosed();
1664
1865
  this.syncPrices();
1665
1866
  this.syncTray();
1666
1867
  return this;
@@ -1712,6 +1913,85 @@ var SeatPicker = class _SeatPicker {
1712
1913
  this.bookedEl = el;
1713
1914
  this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
1714
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
+ }
1715
1995
  // ---- Feature 6: chrome anchor regions -------------------------------------
1716
1996
  /**
1717
1997
  * Create the positioned flex containers that own every persistent map overlay.
@@ -1824,13 +2104,18 @@ var SeatPicker = class _SeatPicker {
1824
2104
  heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
1825
2105
  }
1826
2106
  return gaAreas.reduce(
1827
- (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)),
1828
2108
  0
1829
2109
  );
1830
2110
  }
1831
2111
  syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
1832
2112
  const cta = this.els.cta;
1833
2113
  if (!cta) return;
2114
+ if (this.salesClosed) {
2115
+ cta.disabled = true;
2116
+ cta.textContent = this.tf("picker.salesClosedCta", "Sales closed");
2117
+ return;
2118
+ }
1834
2119
  if (this.confirmSeat) {
1835
2120
  cta.disabled = true;
1836
2121
  cta.textContent = "Confirm or cancel this seat";
@@ -1966,7 +2251,6 @@ var SeatPicker = class _SeatPicker {
1966
2251
  canvas.style.height = `${h}px`;
1967
2252
  wrap.appendChild(canvas);
1968
2253
  (this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
1969
- this.miniEl = wrap;
1970
2254
  this.miniCanvas = canvas;
1971
2255
  const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
1972
2256
  const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
@@ -2079,9 +2363,11 @@ var SeatPicker = class _SeatPicker {
2079
2363
  this.controller.overview();
2080
2364
  }
2081
2365
  // ---- F4 price-band filter -------------------------------------------------
2082
- /** 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. */
2083
2367
  catPrice(c) {
2084
- 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);
2085
2371
  }
2086
2372
  /** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
2087
2373
  priceBands() {
@@ -2126,7 +2412,6 @@ var SeatPicker = class _SeatPicker {
2126
2412
  select.setAttribute("aria-label", "Filter and focus seats by price");
2127
2413
  select.innerHTML = `<option value="all">All prices</option>` + bands.map((band) => `<option value="${band.id}">${band.label}</option>`).join("");
2128
2414
  this.els.pricesSec.appendChild(select);
2129
- this.priceFilterEl = select;
2130
2415
  select.addEventListener("change", () => {
2131
2416
  const band = bands.find((candidate) => candidate.id === select.value);
2132
2417
  const keys = band?.keys ?? null;
@@ -2230,7 +2515,10 @@ var SeatPicker = class _SeatPicker {
2230
2515
  renderSectionCard(summary) {
2231
2516
  if (!this.els.map) return;
2232
2517
  this.secCardEl?.remove();
2233
- 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)}`;
2234
2522
  const leftLabel = (0, import_core2.tCount)("picker.seatsLeftInSection", summary.seatsLeft);
2235
2523
  const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${(0, import_core2.t)("picker.closeSectionSummary")}">\u2715</button>`;
2236
2524
  const card = document.createElement("div");
@@ -2260,7 +2548,7 @@ var SeatPicker = class _SeatPicker {
2260
2548
  card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
2261
2549
  const mix = summary.categories.map((c) => {
2262
2550
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2263
- 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>`;
2264
2552
  }).join("");
2265
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>`;
2266
2554
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
@@ -2327,7 +2615,7 @@ var SeatPicker = class _SeatPicker {
2327
2615
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
2328
2616
  const status = this.controller.getStatus(seat.id) ?? "free";
2329
2617
  const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
2330
- const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
2618
+ const price = cat ? this.catPrice(cat) : void 0;
2331
2619
  this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
2332
2620
  }
2333
2621
  // ---- seat candidate confirmation ------------------------------------------
@@ -2342,7 +2630,8 @@ var SeatPicker = class _SeatPicker {
2342
2630
  if (this.tipEl) this.tipEl.style.display = "none";
2343
2631
  const details = this.controller.seatDetails(seat.id);
2344
2632
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
2345
- 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;
2346
2635
  const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
2347
2636
  "&": "&amp;",
2348
2637
  "<": "&lt;",
@@ -2355,7 +2644,7 @@ var SeatPicker = class _SeatPicker {
2355
2644
  el.setAttribute("aria-modal", "true");
2356
2645
  el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
2357
2646
  el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
2358
- 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>`;
2359
2648
  this.els.map.appendChild(el);
2360
2649
  this.confirmEl = el;
2361
2650
  this.reanchorConfirm();
@@ -2520,18 +2809,35 @@ var SeatPicker = class _SeatPicker {
2520
2809
  }
2521
2810
  // ---- chrome sync ----------------------------------------------------------
2522
2811
  money(n) {
2812
+ const formatter = this.opts.pricing?.formatter;
2813
+ if (formatter) return formatter(n, this.currency);
2523
2814
  try {
2524
2815
  return new Intl.NumberFormat(this.opts.locale, { style: "currency", currency: this.currency }).format(n);
2525
2816
  } catch {
2526
2817
  return `${n} ${this.currency}`;
2527
2818
  }
2528
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
+ }
2529
2833
  syncPrices() {
2530
2834
  const doc = this.controller.doc;
2531
2835
  if (!doc || !this.els.prices) return;
2532
2836
  const left = this.controller.categoryAvailability();
2837
+ this.narrateAvailability(doc.categories, left);
2838
+ this.syncSoldout(doc.categories, left);
2533
2839
  this.els.prices.innerHTML = doc.categories.map((c) => {
2534
- const price = c.tiers?.length ? c.tiers[0].price : c.price;
2840
+ const price = this.catPrice(c);
2535
2841
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2536
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>`;
2537
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>`;
@@ -2540,6 +2846,26 @@ var SeatPicker = class _SeatPicker {
2540
2846
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
2541
2847
  });
2542
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
+ }
2543
2869
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
2544
2870
  evictTakenSelections() {
2545
2871
  const ownLabels = /* @__PURE__ */ new Set([
@@ -2568,15 +2894,23 @@ var SeatPicker = class _SeatPicker {
2568
2894
  const cats = this.controller.doc?.categories ?? [];
2569
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>`);
2570
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>`;
2571
2905
  for (const item of heldItems) {
2572
2906
  const itemKey = `held:${item.label}`;
2573
2907
  nextTrayKeys.add(itemKey);
2574
2908
  const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
2575
2909
  const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
2576
- const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
2577
- 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;
2578
2912
  parts.push(
2579
- `<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>`
2580
2914
  );
2581
2915
  }
2582
2916
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -2585,16 +2919,15 @@ var SeatPicker = class _SeatPicker {
2585
2919
  const itemKey = `seat:${s.id}`;
2586
2920
  nextTrayKeys.add(itemKey);
2587
2921
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
2588
- 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>` : "";
2589
- 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>` : "";
2590
2923
  parts.push(
2591
- `<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>`
2592
2925
  );
2593
2926
  }
2594
2927
  for (const area of gaAreas) {
2595
2928
  const qty = this.gaQty.get(area.id) ?? 0;
2596
2929
  parts.push(
2597
- `<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>`
2598
2931
  );
2599
2932
  }
2600
2933
  this.els.tray.innerHTML = parts.join("");
@@ -2634,7 +2967,7 @@ var SeatPicker = class _SeatPicker {
2634
2967
  return;
2635
2968
  }
2636
2969
  const id = chip.dataset.seat;
2637
- const label = chip.querySelector("b")?.textContent ?? "Seat";
2970
+ const label = this.controller.getSelection().find((sel) => sel.id === id)?.label ?? "Seat";
2638
2971
  const remove = () => {
2639
2972
  this.controller.deselect([id]);
2640
2973
  this.toast(`${label} removed.`, "neutral", {
@@ -2665,6 +2998,11 @@ var SeatPicker = class _SeatPicker {
2665
2998
  if (seat) this.openSeatView(seat);
2666
2999
  });
2667
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
+ });
2668
3006
  this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
2669
3007
  btn.addEventListener("click", () => {
2670
3008
  const areaEl = btn.closest(".sl-ga");
@@ -2677,12 +3015,17 @@ var SeatPicker = class _SeatPicker {
2677
3015
  this.syncTray();
2678
3016
  });
2679
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
+ }
2680
3023
  const gaTotal = this.pendingGATotal(gaAreas);
2681
3024
  const gaCount = this.pendingGACount();
2682
- 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);
2683
3026
  const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
2684
3027
  const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
2685
- 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;
2686
3029
  const count = freshSeats.length + gaCount + heldCount;
2687
3030
  const pendingCount = this.pendingSelectionCount();
2688
3031
  const previousCount = this.lastTrayCount;
@@ -2778,6 +3121,7 @@ var SeatPicker = class _SeatPicker {
2778
3121
  }
2779
3122
  }
2780
3123
  async handleCta() {
3124
+ if (this.salesClosed) return;
2781
3125
  if (this.totalTicketCount() > this.maxTickets) {
2782
3126
  this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
2783
3127
  return;
@@ -2821,6 +3165,7 @@ var SeatPicker = class _SeatPicker {
2821
3165
  this.opts.onError?.(err);
2822
3166
  const problem = err;
2823
3167
  const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
3168
+ if (problem.reason === "event_closed") this.setSalesClosed(true);
2824
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.";
2825
3170
  this.toast(message, "error");
2826
3171
  this.setCtaPhase("idle");
@@ -2929,7 +3274,7 @@ var SeatPicker = class _SeatPicker {
2929
3274
  objectType: it.objectType,
2930
3275
  categoryKey: it.categoryKey,
2931
3276
  tierId: it.tierId,
2932
- unitPrice: it.unitPrice,
3277
+ unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
2933
3278
  currency: it.currency ?? this.currency,
2934
3279
  quantity: it.quantity ?? 1
2935
3280
  }));
@@ -2982,19 +3327,37 @@ var SeatPicker = class _SeatPicker {
2982
3327
  this.tipEl.style.left = `${Math.max(8, x)}px`;
2983
3328
  this.tipEl.style.top = `${Math.max(8, y)}px`;
2984
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
+ }
2985
3348
  updateTooltip(details) {
2986
3349
  if (!this.tipEl) return;
2987
3350
  if (!details) {
2988
3351
  this.tipEl.style.display = "none";
2989
3352
  return;
2990
3353
  }
2991
- 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>`;
2992
- const location = [
2993
- details.sectionLabel ? `Section ${details.sectionLabel}` : "",
2994
- details.rowLabel ? `Row ${details.rowLabel}` : "",
2995
- details.seatNumber ? `Seat ${details.seatNumber}` : details.label
2996
- ].filter(Boolean).join(" \xB7 ");
2997
- 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;
2998
3361
  this.tipEl.style.display = "block";
2999
3362
  this.placeTooltip();
3000
3363
  }
@@ -3015,7 +3378,7 @@ var SeatPicker = class _SeatPicker {
3015
3378
  return this.removeHeldLabel(label);
3016
3379
  }
3017
3380
  async bestAvailable(qty, categoryKey) {
3018
- if (this.bestAvailableBusy) return null;
3381
+ if (this.salesClosed || this.bestAvailableBusy) return null;
3019
3382
  qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
3020
3383
  if (this.confirmSeat) this.cancelConfirm();
3021
3384
  this.bestAvailableConfirm = false;
@@ -3096,6 +3459,8 @@ var SeatPicker = class _SeatPicker {
3096
3459
  this.ro?.disconnect();
3097
3460
  this.ro = null;
3098
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);
3099
3464
  this.controller.destroy();
3100
3465
  this.root?.remove();
3101
3466
  this.root = null;