@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.js CHANGED
@@ -662,6 +662,7 @@ import {
662
662
  PickerController as PickerController2,
663
663
  expandChart,
664
664
  generateSeatPanorama,
665
+ generateSeatThumb,
665
666
  loadLocale as loadLocale2,
666
667
  setStringOverrides as setStringOverrides2,
667
668
  t as t2,
@@ -826,26 +827,43 @@ var CSS = `
826
827
  .sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
827
828
  overscroll-behavior:contain;scrollbar-gutter:stable}
828
829
  .sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
829
- .sl-chip{position:relative;display:grid;grid-template-columns:24px minmax(0,1fr) auto 30px;align-items:center;gap:8px;
830
- 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;
831
832
  background:var(--sl-surface);font-size:13px;transform-origin:center;transition:border-color .15s,background .15s}
832
833
  .sl-chip:hover{border-color:color-mix(in srgb,var(--sl-accent) 38%,var(--sl-line))}
833
834
  .sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
834
835
  .sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
835
836
  .sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
836
837
  box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
837
- .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;
838
839
  background:var(--sl-accent);color:var(--sl-accent-ink)}
839
840
  .sl-ticket-state.held{background:color-mix(in srgb,var(--sl-accent) 18%,var(--sl-surface));color:var(--sl-accent)}
840
- .sl-ticket-state svg{width:13px;height:13px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
841
- .sl-chip-main{min-width:0}
842
- .sl-chip b{display:block;font-weight:800;min-width:max-content;white-space:nowrap}
843
- .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}
844
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}
845
852
  .sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums;flex:none;white-space:nowrap}
846
- .sl-chip .rm{width:29px;height:29px;border-radius:8px;flex:none;display:flex;align-items:center;justify-content:center;color:var(--sl-muted)}
847
- .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)}
848
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}
849
867
 
850
868
  /* GA rows */
851
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)}
@@ -921,6 +939,8 @@ var CSS = `
921
939
 
922
940
  /* zoom column (flows within the bottom-right region) */
923
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}
924
944
  .sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
925
945
  color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
926
946
  .sl-zoom button:hover{border-color:var(--sl-muted)}
@@ -979,6 +999,39 @@ var CSS = `
979
999
  .sl-booked.on .sl-booked-title{animation-delay:.22s}
980
1000
  .sl-booked.on .sl-booked-sub{animation-delay:.3s}
981
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
+
982
1035
  /* a11y filter chips (flow within the top-left region) */
983
1036
  .sl-chips{display:flex;gap:6px;flex-wrap:wrap}
984
1037
  .sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
@@ -1017,8 +1070,30 @@ var CSS = `
1017
1070
  .sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
1018
1071
  transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
1019
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
+
1020
1095
  /* Best available is a first-class shortcut, not an anonymous utility row. */
1021
- .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;
1022
1097
  padding:13px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));border-radius:13px;
1023
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)))}
1024
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}
@@ -1054,12 +1129,6 @@ var CSS = `
1054
1129
  /* per-seat ticket-tier select + view-from-seat button in tray chips */
1055
1130
  .sl-chip .tier{background:var(--sl-bg);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:6px;
1056
1131
  font:inherit;font-size:10px;padding:2px 4px;min-width:0;max-width:100%;cursor:pointer}
1057
- .sl-chip .view{width:20px;height:20px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
1058
- color:var(--sl-muted);opacity:.36;transition:color .15s,opacity .15s}
1059
- .sl-chip .view:hover{color:var(--sl-text)}
1060
- .sl-chip:hover .view,.sl-chip .view:focus-visible{opacity:1;color:var(--sl-text)}
1061
- .sl-chip .view svg{width:12px;height:12px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
1062
- @media(pointer:coarse){.sl-chip .view{opacity:.58}}
1063
1132
 
1064
1133
  /* arena: LOD rung pills (flow within the top-center region) */
1065
1134
  .sl-rungs{display:none;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
@@ -1117,6 +1186,14 @@ var CSS = `
1117
1186
  .sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
1118
1187
 
1119
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}
1120
1197
  .sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
1121
1198
  color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
1122
1199
  .sl-confirm-view:hover{border-color:var(--sl-muted)}
@@ -1200,6 +1277,22 @@ function resolveTokens(chart, host) {
1200
1277
  "--sl-radius": `${host?.radius ?? 14}px`
1201
1278
  };
1202
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
+ }
1203
1296
  var SeatPicker = class _SeatPicker {
1204
1297
  constructor(options) {
1205
1298
  this.root = null;
@@ -1232,11 +1325,17 @@ var SeatPicker = class _SeatPicker {
1232
1325
  this.confirmEl = null;
1233
1326
  this.confirmSeat = null;
1234
1327
  this.srEl = null;
1235
- this.a11yFilter = "all";
1236
1328
  this.baQty = 2;
1237
1329
  this.baCat = "";
1238
1330
  this.bestAvailableConfirm = false;
1239
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;
1240
1339
  // arena / multi-floor / seat-view chrome
1241
1340
  this.rungsEl = null;
1242
1341
  this.floorsEl = null;
@@ -1245,13 +1344,11 @@ var SeatPicker = class _SeatPicker {
1245
1344
  this.viewCleanup = null;
1246
1345
  this.allSeatsCache = null;
1247
1346
  // F3 minimap
1248
- this.miniEl = null;
1249
1347
  this.miniCanvas = null;
1250
1348
  this.miniBase = null;
1251
1349
  this.miniTf = null;
1252
1350
  // F4 price-band filter — active band's category keys (null = all prices)
1253
1351
  this.priceBandKeys = null;
1254
- this.priceFilterEl = null;
1255
1352
  /** Last surfaced section summary (re-rendered when the price band changes). */
1256
1353
  this.lastSection = null;
1257
1354
  /** Section card collapsed to its slim pill (seat-picking has begun). */
@@ -1271,6 +1368,9 @@ var SeatPicker = class _SeatPicker {
1271
1368
  this.ctaPhase = "idle";
1272
1369
  // narrow-layout chrome that docks into the sheet's Filters row on mobile
1273
1370
  this.a11yChipsEl = null;
1371
+ this.fsFallback = false;
1372
+ this.fsChangeHandler = null;
1373
+ this.fsEscHandler = null;
1274
1374
  this.cbEl = null;
1275
1375
  // modal plumbing (set by open())
1276
1376
  this.modalScrim = null;
@@ -1278,20 +1378,22 @@ var SeatPicker = class _SeatPicker {
1278
1378
  this.escHandler = null;
1279
1379
  /** Set by open(): closes the modal (scroll restore + destroy + onClose). */
1280
1380
  this.closeModal = null;
1381
+ this.lastCatAvail = null;
1281
1382
  if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
1282
1383
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
1283
1384
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
1284
1385
  this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
1285
1386
  this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
1286
- this.api = new PubApi(this.apiBase);
1387
+ this.api = options.transport ?? new PubApi(this.apiBase);
1287
1388
  this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
1389
+ this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
1288
1390
  this.controller = new PickerController2({
1289
1391
  transport: this.api,
1290
1392
  eventKey: options.event,
1291
1393
  maxSelection: this.maxTickets,
1292
1394
  currency: options.currency,
1293
1395
  flashOnLiveChange: true,
1294
- colorblindSafe: options.colorblindSafe,
1396
+ colorblindSafe: this.cbSafe,
1295
1397
  onSelectionChange: () => {
1296
1398
  this.syncTray();
1297
1399
  if (this.committedSelection().length) this.collapseSectionCard();
@@ -1317,6 +1419,11 @@ var SeatPicker = class _SeatPicker {
1317
1419
  },
1318
1420
  confirmSelection: this.opts.confirmSelection,
1319
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
+ }
1320
1427
  this.flashPickedSeat(seat.id);
1321
1428
  if (this.opts.confirmSelection) this.showConfirm(seat);
1322
1429
  },
@@ -1339,9 +1446,68 @@ var SeatPicker = class _SeatPicker {
1339
1446
  onHint: (m) => {
1340
1447
  if (m) this.toast(m);
1341
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),
1342
1452
  onError: (err) => this.opts.onError?.(err)
1343
1453
  });
1344
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
+ }
1345
1511
  /**
1346
1512
  * Close the picker. In modal mode (SeatPicker.open()) this dismisses the
1347
1513
  * modal exactly like ESC/scrim/✕ — restores page scroll and fires onClose.
@@ -1436,6 +1602,10 @@ var SeatPicker = class _SeatPicker {
1436
1602
  <div class="sl-head-meta" data-ref="meta"></div>
1437
1603
  </div>
1438
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>
1439
1609
  <button type="button" class="sl-close" data-ref="close" aria-label="Close">
1440
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>
1441
1611
  </button>
@@ -1449,6 +1619,9 @@ var SeatPicker = class _SeatPicker {
1449
1619
  <button type="button" aria-label="Fit to screen" data-ref="zfit">
1450
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>
1451
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>
1452
1625
  </div>
1453
1626
  <div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
1454
1627
  <div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
@@ -1467,6 +1640,7 @@ var SeatPicker = class _SeatPicker {
1467
1640
  <div class="sl-filters" data-ref="filters"></div>
1468
1641
  <div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>Ticket prices</span></div>
1469
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>
1470
1644
  <div class="sl-sec sl-seats-sec"><span>Your seats</span><span class="sl-seat-summary" data-ref="seatSummary"></span></div>
1471
1645
  <div class="sl-tray" data-ref="tray"></div>
1472
1646
  <div class="sl-foot" data-ref="foot">
@@ -1500,6 +1674,13 @@ var SeatPicker = class _SeatPicker {
1500
1674
  this.els.zin.addEventListener("click", () => this.controller.zoomIn());
1501
1675
  this.els.zout.addEventListener("click", () => this.controller.zoomOut());
1502
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);
1503
1684
  const head = this.els.sheetHead;
1504
1685
  if (head) {
1505
1686
  const toggle = this.els.sheetToggle;
@@ -1543,7 +1724,7 @@ var SeatPicker = class _SeatPicker {
1543
1724
  }
1544
1725
  this.tipEl = document.createElement("div");
1545
1726
  this.tipEl.setAttribute("role", "tooltip");
1546
- 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";
1547
1728
  this.els.map.appendChild(this.tipEl);
1548
1729
  this.els.map.addEventListener("mousemove", (e) => {
1549
1730
  const r = this.els.map.getBoundingClientRect();
@@ -1568,6 +1749,7 @@ var SeatPicker = class _SeatPicker {
1568
1749
  return this;
1569
1750
  }
1570
1751
  this.els.boot.remove();
1752
+ this.salesClosed = !!info.salesClosed;
1571
1753
  this.buildRegions();
1572
1754
  this.regions["bottom-right"].appendChild(this.els.zoom);
1573
1755
  this.regions["bottom-center"].appendChild(this.els.toast);
@@ -1587,6 +1769,7 @@ var SeatPicker = class _SeatPicker {
1587
1769
  this.els.name.textContent = info.eventName ?? "";
1588
1770
  const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
1589
1771
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
1772
+ this.buildBadge(chartTheme);
1590
1773
  const present = /* @__PURE__ */ new Set();
1591
1774
  if (this.controller.doc) {
1592
1775
  for (const seat of expandChart(this.controller.doc)) {
@@ -1602,12 +1785,29 @@ var SeatPicker = class _SeatPicker {
1602
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("");
1603
1786
  this.regions["top-left"].appendChild(chips);
1604
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
+ };
1605
1804
  chips.querySelectorAll("button").forEach((btn) => {
1606
1805
  btn.addEventListener("click", () => {
1607
1806
  const f = btn.dataset.f;
1608
- this.a11yFilter = f;
1609
- chips.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
1610
- 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();
1611
1811
  });
1612
1812
  });
1613
1813
  }
@@ -1616,14 +1816,14 @@ var SeatPicker = class _SeatPicker {
1616
1816
  cb.className = "sl-cbbtn";
1617
1817
  this.cbEl = cb;
1618
1818
  cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
1619
- cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
1819
+ cb.setAttribute("aria-pressed", String(this.cbSafe));
1620
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>';
1621
1821
  this.els.zfit.parentElement.appendChild(cb);
1622
- let cbOn = !!this.opts.colorblindSafe;
1623
1822
  cb.addEventListener("click", () => {
1624
- cbOn = !cbOn;
1625
- cb.setAttribute("aria-pressed", String(cbOn));
1626
- 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);
1627
1827
  });
1628
1828
  this.srEl = document.createElement("div");
1629
1829
  this.srEl.className = "sl-sr";
@@ -1634,9 +1834,11 @@ var SeatPicker = class _SeatPicker {
1634
1834
  this.buildPriceFilter();
1635
1835
  this.buildExtendPrompt();
1636
1836
  this.buildBookedOverlay();
1837
+ this.buildSoldoutOverlay();
1637
1838
  this.dockLayoutChrome();
1638
1839
  await this.restoreRememberedHold();
1639
1840
  if (this.destroyed) return this;
1841
+ if (this.salesClosed) this.applySalesClosed();
1640
1842
  this.syncPrices();
1641
1843
  this.syncTray();
1642
1844
  return this;
@@ -1688,6 +1890,85 @@ var SeatPicker = class _SeatPicker {
1688
1890
  this.bookedEl = el;
1689
1891
  this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
1690
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
+ }
1691
1972
  // ---- Feature 6: chrome anchor regions -------------------------------------
1692
1973
  /**
1693
1974
  * Create the positioned flex containers that own every persistent map overlay.
@@ -1800,13 +2081,18 @@ var SeatPicker = class _SeatPicker {
1800
2081
  heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
1801
2082
  }
1802
2083
  return gaAreas.reduce(
1803
- (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)),
1804
2085
  0
1805
2086
  );
1806
2087
  }
1807
2088
  syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
1808
2089
  const cta = this.els.cta;
1809
2090
  if (!cta) return;
2091
+ if (this.salesClosed) {
2092
+ cta.disabled = true;
2093
+ cta.textContent = this.tf("picker.salesClosedCta", "Sales closed");
2094
+ return;
2095
+ }
1810
2096
  if (this.confirmSeat) {
1811
2097
  cta.disabled = true;
1812
2098
  cta.textContent = "Confirm or cancel this seat";
@@ -1942,7 +2228,6 @@ var SeatPicker = class _SeatPicker {
1942
2228
  canvas.style.height = `${h}px`;
1943
2229
  wrap.appendChild(canvas);
1944
2230
  (this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
1945
- this.miniEl = wrap;
1946
2231
  this.miniCanvas = canvas;
1947
2232
  const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
1948
2233
  const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
@@ -2055,9 +2340,11 @@ var SeatPicker = class _SeatPicker {
2055
2340
  this.controller.overview();
2056
2341
  }
2057
2342
  // ---- F4 price-band filter -------------------------------------------------
2058
- /** 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. */
2059
2344
  catPrice(c) {
2060
- 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);
2061
2348
  }
2062
2349
  /** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
2063
2350
  priceBands() {
@@ -2102,7 +2389,6 @@ var SeatPicker = class _SeatPicker {
2102
2389
  select.setAttribute("aria-label", "Filter and focus seats by price");
2103
2390
  select.innerHTML = `<option value="all">All prices</option>` + bands.map((band) => `<option value="${band.id}">${band.label}</option>`).join("");
2104
2391
  this.els.pricesSec.appendChild(select);
2105
- this.priceFilterEl = select;
2106
2392
  select.addEventListener("change", () => {
2107
2393
  const band = bands.find((candidate) => candidate.id === select.value);
2108
2394
  const keys = band?.keys ?? null;
@@ -2206,7 +2492,10 @@ var SeatPicker = class _SeatPicker {
2206
2492
  renderSectionCard(summary) {
2207
2493
  if (!this.els.map) return;
2208
2494
  this.secCardEl?.remove();
2209
- 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)}`;
2210
2499
  const leftLabel = tCount("picker.seatsLeftInSection", summary.seatsLeft);
2211
2500
  const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${t2("picker.closeSectionSummary")}">\u2715</button>`;
2212
2501
  const card = document.createElement("div");
@@ -2236,7 +2525,7 @@ var SeatPicker = class _SeatPicker {
2236
2525
  card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
2237
2526
  const mix = summary.categories.map((c) => {
2238
2527
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2239
- 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>`;
2240
2529
  }).join("");
2241
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>`;
2242
2531
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
@@ -2303,7 +2592,7 @@ var SeatPicker = class _SeatPicker {
2303
2592
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
2304
2593
  const status = this.controller.getStatus(seat.id) ?? "free";
2305
2594
  const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
2306
- const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
2595
+ const price = cat ? this.catPrice(cat) : void 0;
2307
2596
  this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
2308
2597
  }
2309
2598
  // ---- seat candidate confirmation ------------------------------------------
@@ -2318,7 +2607,8 @@ var SeatPicker = class _SeatPicker {
2318
2607
  if (this.tipEl) this.tipEl.style.display = "none";
2319
2608
  const details = this.controller.seatDetails(seat.id);
2320
2609
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
2321
- 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;
2322
2612
  const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
2323
2613
  "&": "&amp;",
2324
2614
  "<": "&lt;",
@@ -2331,7 +2621,7 @@ var SeatPicker = class _SeatPicker {
2331
2621
  el.setAttribute("aria-modal", "true");
2332
2622
  el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
2333
2623
  el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
2334
- 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>`;
2335
2625
  this.els.map.appendChild(el);
2336
2626
  this.confirmEl = el;
2337
2627
  this.reanchorConfirm();
@@ -2496,18 +2786,35 @@ var SeatPicker = class _SeatPicker {
2496
2786
  }
2497
2787
  // ---- chrome sync ----------------------------------------------------------
2498
2788
  money(n) {
2789
+ const formatter = this.opts.pricing?.formatter;
2790
+ if (formatter) return formatter(n, this.currency);
2499
2791
  try {
2500
2792
  return new Intl.NumberFormat(this.opts.locale, { style: "currency", currency: this.currency }).format(n);
2501
2793
  } catch {
2502
2794
  return `${n} ${this.currency}`;
2503
2795
  }
2504
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
+ }
2505
2810
  syncPrices() {
2506
2811
  const doc = this.controller.doc;
2507
2812
  if (!doc || !this.els.prices) return;
2508
2813
  const left = this.controller.categoryAvailability();
2814
+ this.narrateAvailability(doc.categories, left);
2815
+ this.syncSoldout(doc.categories, left);
2509
2816
  this.els.prices.innerHTML = doc.categories.map((c) => {
2510
- const price = c.tiers?.length ? c.tiers[0].price : c.price;
2817
+ const price = this.catPrice(c);
2511
2818
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2512
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>`;
2513
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>`;
@@ -2516,6 +2823,26 @@ var SeatPicker = class _SeatPicker {
2516
2823
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
2517
2824
  });
2518
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
+ }
2519
2846
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
2520
2847
  evictTakenSelections() {
2521
2848
  const ownLabels = /* @__PURE__ */ new Set([
@@ -2544,15 +2871,23 @@ var SeatPicker = class _SeatPicker {
2544
2871
  const cats = this.controller.doc?.categories ?? [];
2545
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>`);
2546
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>`;
2547
2882
  for (const item of heldItems) {
2548
2883
  const itemKey = `held:${item.label}`;
2549
2884
  nextTrayKeys.add(itemKey);
2550
2885
  const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
2551
2886
  const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
2552
- const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
2553
- 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;
2554
2889
  parts.push(
2555
- `<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>`
2556
2891
  );
2557
2892
  }
2558
2893
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -2561,16 +2896,15 @@ var SeatPicker = class _SeatPicker {
2561
2896
  const itemKey = `seat:${s.id}`;
2562
2897
  nextTrayKeys.add(itemKey);
2563
2898
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
2564
- 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>` : "";
2565
- 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>` : "";
2566
2900
  parts.push(
2567
- `<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>`
2568
2902
  );
2569
2903
  }
2570
2904
  for (const area of gaAreas) {
2571
2905
  const qty = this.gaQty.get(area.id) ?? 0;
2572
2906
  parts.push(
2573
- `<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>`
2574
2908
  );
2575
2909
  }
2576
2910
  this.els.tray.innerHTML = parts.join("");
@@ -2610,7 +2944,7 @@ var SeatPicker = class _SeatPicker {
2610
2944
  return;
2611
2945
  }
2612
2946
  const id = chip.dataset.seat;
2613
- const label = chip.querySelector("b")?.textContent ?? "Seat";
2947
+ const label = this.controller.getSelection().find((sel) => sel.id === id)?.label ?? "Seat";
2614
2948
  const remove = () => {
2615
2949
  this.controller.deselect([id]);
2616
2950
  this.toast(`${label} removed.`, "neutral", {
@@ -2641,6 +2975,11 @@ var SeatPicker = class _SeatPicker {
2641
2975
  if (seat) this.openSeatView(seat);
2642
2976
  });
2643
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
+ });
2644
2983
  this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
2645
2984
  btn.addEventListener("click", () => {
2646
2985
  const areaEl = btn.closest(".sl-ga");
@@ -2653,12 +2992,17 @@ var SeatPicker = class _SeatPicker {
2653
2992
  this.syncTray();
2654
2993
  });
2655
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
+ }
2656
3000
  const gaTotal = this.pendingGATotal(gaAreas);
2657
3001
  const gaCount = this.pendingGACount();
2658
- 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);
2659
3003
  const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
2660
3004
  const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
2661
- 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;
2662
3006
  const count = freshSeats.length + gaCount + heldCount;
2663
3007
  const pendingCount = this.pendingSelectionCount();
2664
3008
  const previousCount = this.lastTrayCount;
@@ -2754,6 +3098,7 @@ var SeatPicker = class _SeatPicker {
2754
3098
  }
2755
3099
  }
2756
3100
  async handleCta() {
3101
+ if (this.salesClosed) return;
2757
3102
  if (this.totalTicketCount() > this.maxTickets) {
2758
3103
  this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
2759
3104
  return;
@@ -2797,6 +3142,7 @@ var SeatPicker = class _SeatPicker {
2797
3142
  this.opts.onError?.(err);
2798
3143
  const problem = err;
2799
3144
  const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
3145
+ if (problem.reason === "event_closed") this.setSalesClosed(true);
2800
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.";
2801
3147
  this.toast(message, "error");
2802
3148
  this.setCtaPhase("idle");
@@ -2905,7 +3251,7 @@ var SeatPicker = class _SeatPicker {
2905
3251
  objectType: it.objectType,
2906
3252
  categoryKey: it.categoryKey,
2907
3253
  tierId: it.tierId,
2908
- unitPrice: it.unitPrice,
3254
+ unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
2909
3255
  currency: it.currency ?? this.currency,
2910
3256
  quantity: it.quantity ?? 1
2911
3257
  }));
@@ -2958,19 +3304,37 @@ var SeatPicker = class _SeatPicker {
2958
3304
  this.tipEl.style.left = `${Math.max(8, x)}px`;
2959
3305
  this.tipEl.style.top = `${Math.max(8, y)}px`;
2960
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
+ }
2961
3325
  updateTooltip(details) {
2962
3326
  if (!this.tipEl) return;
2963
3327
  if (!details) {
2964
3328
  this.tipEl.style.display = "none";
2965
3329
  return;
2966
3330
  }
2967
- 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>`;
2968
- const location = [
2969
- details.sectionLabel ? `Section ${details.sectionLabel}` : "",
2970
- details.rowLabel ? `Row ${details.rowLabel}` : "",
2971
- details.seatNumber ? `Seat ${details.seatNumber}` : details.label
2972
- ].filter(Boolean).join(" \xB7 ");
2973
- 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;
2974
3338
  this.tipEl.style.display = "block";
2975
3339
  this.placeTooltip();
2976
3340
  }
@@ -2991,7 +3355,7 @@ var SeatPicker = class _SeatPicker {
2991
3355
  return this.removeHeldLabel(label);
2992
3356
  }
2993
3357
  async bestAvailable(qty, categoryKey) {
2994
- if (this.bestAvailableBusy) return null;
3358
+ if (this.salesClosed || this.bestAvailableBusy) return null;
2995
3359
  qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
2996
3360
  if (this.confirmSeat) this.cancelConfirm();
2997
3361
  this.bestAvailableConfirm = false;
@@ -3072,6 +3436,8 @@ var SeatPicker = class _SeatPicker {
3072
3436
  this.ro?.disconnect();
3073
3437
  this.ro = null;
3074
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);
3075
3441
  this.controller.destroy();
3076
3442
  this.root?.remove();
3077
3443
  this.root = null;